diff --git a/i18n/en.pot b/i18n/en.pot index 7d1166cbe..9e96f306e 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: 2025-10-20T15:52:19.670Z\n" -"PO-Revision-Date: 2025-10-20T15:52:19.670Z\n" +"POT-Creation-Date: 2025-10-21T03:21:20.753Z\n" +"PO-Revision-Date: 2025-10-21T03:21:20.753Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " @@ -1986,6 +1986,18 @@ msgstr "" msgid "Leave empty to keep all history" msgstr "" +msgid "Metadata Sync User Settings" +msgstr "" + +msgid "Default owner and sharing settings inclusion method" +msgstr "" + +msgid "Default users inclusion method" +msgstr "" + +msgid "Default organisation units inclusion method" +msgstr "" + msgid "Data Store" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 49b0d4864..526bfc8f5 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2025-10-16T11:09:45.206Z\n" +"POT-Creation-Date: 2025-10-21T03:21:20.753Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1988,6 +1988,18 @@ msgstr "" msgid "Leave empty to keep all history" msgstr "" +msgid "Metadata Sync User Settings" +msgstr "" + +msgid "Default owner and sharing settings inclusion method" +msgstr "" + +msgid "Default users inclusion method" +msgstr "" + +msgid "Default organisation units inclusion method" +msgstr "" + msgid "Data Store" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 1adaee730..9bc1c1cab 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2025-10-16T11:09:45.206Z\n" +"POT-Creation-Date: 2025-10-21T03:21:20.753Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1988,6 +1988,18 @@ msgstr "" msgid "Leave empty to keep all history" msgstr "" +msgid "Metadata Sync User Settings" +msgstr "" + +msgid "Default owner and sharing settings inclusion method" +msgstr "" + +msgid "Default users inclusion method" +msgstr "" + +msgid "Default organisation units inclusion method" +msgstr "" + msgid "Data Store" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 1adaee730..9bc1c1cab 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2025-10-16T11:09:45.206Z\n" +"POT-Creation-Date: 2025-10-21T03:21:20.753Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1988,6 +1988,18 @@ msgstr "" msgid "Leave empty to keep all history" msgstr "" +msgid "Metadata Sync User Settings" +msgstr "" + +msgid "Default owner and sharing settings inclusion method" +msgstr "" + +msgid "Default users inclusion method" +msgstr "" + +msgid "Default organisation units inclusion method" +msgstr "" + msgid "Data Store" msgstr "" diff --git a/src/data/storage/Namespaces.ts b/src/data/storage/Namespaces.ts index bef4ec10f..47f3bba45 100644 --- a/src/data/storage/Namespaces.ts +++ b/src/data/storage/Namespaces.ts @@ -11,6 +11,7 @@ export const Namespace = { RESPONSIBLES: "responsibles", MAPPINGS: "mappings", SETTINGS: "settings", + USER_SETTINGS: "user-settings", SCHEDULER_EXECUTIONS: "scheduler-executions", EVENTS_USER_COLUMNS: "events-user-columns", }; diff --git a/src/data/user-settings/UserSettingsD2ApiRepository.ts b/src/data/user-settings/UserSettingsD2ApiRepository.ts new file mode 100644 index 000000000..da4618a67 --- /dev/null +++ b/src/data/user-settings/UserSettingsD2ApiRepository.ts @@ -0,0 +1,37 @@ +import { Future, FutureData } from "../../domain/common/entities/Future"; + +import { Namespace } from "../storage/Namespaces"; +import { Instance } from "../../domain/instance/entities/Instance"; +import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; +import { DEFAULT_USER_SETTINGS, UserSettings, UserSettingsProps } from "../../domain/user-settings/UserSettings"; +import { UserSettingsRepository } from "../../domain/user-settings/UserSettingsRepository"; + +export class UserSettingsD2ApiRepository implements UserSettingsRepository { + private dataStoreClient: StorageDataStoreClient; + constructor(private instance: Instance) { + this.dataStoreClient = new StorageDataStoreClient(this.instance, undefined, { storageType: "user" }); + } + + get(): FutureData { + return this.dataStoreClient + .getObjectFuture(Namespace.USER_SETTINGS) + .flatMap(userSettings => { + if (!userSettings) { + return this.dataStoreClient + .saveObjectFuture(Namespace.USER_SETTINGS, DEFAULT_USER_SETTINGS) + .flatMap(() => { + return Future.success(UserSettings.create(DEFAULT_USER_SETTINGS)); + }); + } else { + return Future.success(UserSettings.create(userSettings)); + } + }); + } + + save(userSettings: UserSettings): FutureData { + return this.dataStoreClient.saveObjectFuture( + Namespace.USER_SETTINGS, + userSettings._getAttributes() + ); + } +} diff --git a/src/domain/rules/entities/SynchronizationRule.ts b/src/domain/rules/entities/SynchronizationRule.ts index 47c54e1b2..576ec28ef 100644 --- a/src/domain/rules/entities/SynchronizationRule.ts +++ b/src/domain/rules/entities/SynchronizationRule.ts @@ -22,6 +22,7 @@ import { import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; import { TeisSyncPeriodField } from "../../aggregated/entities/TeisSyncPeriodField"; import { EventsSyncPeriodField } from "../../aggregated/entities/EventsSyncPeriodField"; +import { UserSettingsInclusionsConfig } from "../../user-settings/UserSettings"; export class SynchronizationRule { private readonly syncRule: SynchronizationRuleData; @@ -305,8 +306,17 @@ export class SynchronizationRule { }); } - public static createOnDemand(type: SynchronizationType = "metadata"): SynchronizationRule { - return SynchronizationRule.create(type).updateName("__MANUAL__").updateOndemand(true); + public static createOnDemand( + type: SynchronizationType = "metadata", + defaultInclusions?: UserSettingsInclusionsConfig + ): SynchronizationRule { + const onDemandRule = SynchronizationRule.create(type).updateName("__MANUAL__").updateOndemand(true); + + if (defaultInclusions) { + return onDemandRule.setDefaultInclusions(defaultInclusions); + } else { + return onDemandRule; + } } public static build(syncRule: SynchronizationRuleData | undefined): SynchronizationRule { @@ -765,6 +775,19 @@ export class SynchronizationRule { return isUserOwner || isPublic || hasUserAccess || hasGroupAccess; } + public setDefaultInclusions(defaultInclusions: UserSettingsInclusionsConfig) { + const { sharing, users, organisationUnits } = defaultInclusions; + return this.updateSyncParams({ + ...this.syncParams, + includeSharingSettingsObjectsAndReferences: sharing === "includeObjectsAndReferences", + includeOnlySharingSettingsReferences: sharing === "includeOnlyReferences", + includeUsersObjectsAndReferences: users === "includeObjectsAndReferences", + includeOnlyUsersReferences: users === "includeOnlyReferences", + includeOrgUnitsObjectsAndReferences: organisationUnits === "includeObjectsAndReferences", + includeOnlyOrgUnitsReferences: organisationUnits === "includeOnlyReferences", + }); + } + private get usesFilterRules(): boolean { return this.type === "metadata"; } diff --git a/src/domain/user-settings/GetUserSettingsUseCase.ts b/src/domain/user-settings/GetUserSettingsUseCase.ts new file mode 100644 index 000000000..560604ba2 --- /dev/null +++ b/src/domain/user-settings/GetUserSettingsUseCase.ts @@ -0,0 +1,11 @@ +import { FutureData } from "../common/entities/Future"; +import { UserSettings } from "./UserSettings"; +import { UserSettingsRepository } from "./UserSettingsRepository"; + +export class GetUserSettingsUseCase { + constructor(private userSettingsRepository: UserSettingsRepository) {} + + public execute(): FutureData { + return this.userSettingsRepository.get(); + } +} diff --git a/src/domain/user-settings/SaveUserSettingsUseCase.ts b/src/domain/user-settings/SaveUserSettingsUseCase.ts new file mode 100644 index 000000000..8677f4128 --- /dev/null +++ b/src/domain/user-settings/SaveUserSettingsUseCase.ts @@ -0,0 +1,11 @@ +import { FutureData } from "../common/entities/Future"; +import { UserSettings } from "./UserSettings"; +import { UserSettingsRepository } from "./UserSettingsRepository"; + +export class SaveUserSettingsUseCase { + constructor(private userSettingsRepository: UserSettingsRepository) {} + + public execute(userSettings: UserSettings): FutureData { + return this.userSettingsRepository.save(userSettings); + } +} diff --git a/src/domain/user-settings/UserSettings.ts b/src/domain/user-settings/UserSettings.ts new file mode 100644 index 000000000..4cf6dae68 --- /dev/null +++ b/src/domain/user-settings/UserSettings.ts @@ -0,0 +1,36 @@ +import { Struct } from "../common/entities/Struct"; + +export const DEFAULT_INCLUSION_MODE = "includeObjectsAndReferences"; +export const DEFAULT_USER_SETTINGS: UserSettingsProps = { + inclusionConfig: { + sharing: DEFAULT_INCLUSION_MODE, + users: DEFAULT_INCLUSION_MODE, + organisationUnits: DEFAULT_INCLUSION_MODE, + }, +}; + +const inclusionModes = ["includeObjectsAndReferences", "includeOnlyReferences", "removeObjectsAndReferences"] as const; + +export type InclusionMode = typeof inclusionModes[number]; + +export type UserSettingsProps = { + inclusionConfig: { + sharing: InclusionMode; + users: InclusionMode; + organisationUnits: InclusionMode; + }; +}; +export type UserSettingsInclusionsConfig = UserSettingsProps["inclusionConfig"]; + +export class UserSettings extends Struct() { + updateInclusionConfig( + key: K, + value: UserSettings["inclusionConfig"][K] + ): UserSettings { + return this._update({ inclusionConfig: { ...this.inclusionConfig, [key]: value } }); + } + + static default(): UserSettings { + return UserSettings.create(DEFAULT_USER_SETTINGS); + } +} diff --git a/src/domain/user-settings/UserSettingsRepository.ts b/src/domain/user-settings/UserSettingsRepository.ts new file mode 100644 index 000000000..33ae97d09 --- /dev/null +++ b/src/domain/user-settings/UserSettingsRepository.ts @@ -0,0 +1,7 @@ +import { FutureData } from "../common/entities/Future"; +import { UserSettings } from "./UserSettings"; + +export interface UserSettingsRepository { + get(): FutureData; + save(userSettings: UserSettings): FutureData; +} diff --git a/src/presentation/NewCompositionRoot.ts b/src/presentation/NewCompositionRoot.ts index 9c27d6ebd..6f9c8b0db 100644 --- a/src/presentation/NewCompositionRoot.ts +++ b/src/presentation/NewCompositionRoot.ts @@ -7,6 +7,10 @@ import { SettingsRepository } from "../domain/settings/SettingsRepository"; import { GetSettingsUseCase } from "../domain/settings/GetSettingsUseCase"; import { SaveSettingsUseCase } from "../domain/settings/SaveSettingsUseCase"; import { SettingsD2ApiRepository } from "../data/settings/SettingsD2ApiRepository"; +import { UserSettingsRepository } from "../domain/user-settings/UserSettingsRepository"; +import { UserSettingsD2ApiRepository } from "../data/user-settings/UserSettingsD2ApiRepository"; +import { GetUserSettingsUseCase } from "../domain/user-settings/GetUserSettingsUseCase"; +import { SaveUserSettingsUseCase } from "../domain/user-settings/SaveUserSettingsUseCase"; /** * @description This file is refactored @@ -17,6 +21,7 @@ export type NewCompositionRoot = ReturnType; type Repositories = { storageClientRepository: StorageClientRepository; settingsRepository: SettingsRepository; + userSettingsRepository: UserSettingsRepository; }; function getCompositionRoot(repositories: Repositories) { @@ -30,6 +35,11 @@ function getCompositionRoot(repositories: Repositories) { get: new GetSettingsUseCase(repositories.settingsRepository), save: new SaveSettingsUseCase(repositories.settingsRepository), }, + + userSettings: { + get: new GetUserSettingsUseCase(repositories.userSettingsRepository), + save: new SaveUserSettingsUseCase(repositories.userSettingsRepository), + }, }; } @@ -38,6 +48,7 @@ export function getWebappCompositionRoot(instance: Instance) { const repositories: Repositories = { storageClientRepository: new StorageClientD2Repository(instance), settingsRepository: new SettingsD2ApiRepository(storageClientRepository), + userSettingsRepository: new UserSettingsD2ApiRepository(instance), }; return getCompositionRoot(repositories); diff --git a/src/presentation/react/core/components/sync-wizard/metadata/InclusionFields.tsx b/src/presentation/react/core/components/sync-wizard/metadata/InclusionFields.tsx new file mode 100644 index 000000000..29870111d --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/metadata/InclusionFields.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import Dropdown from "../../dropdown/Dropdown"; +import styled from "styled-components"; +import i18n from "../../../../../../utils/i18n"; +import { InclusionMode } from "../../../../../../domain/user-settings/UserSettings"; + +export type InclusionFieldsProps = { + sharingSettings: { + value: InclusionMode; + options: { id: InclusionMode; name: string }[]; + onValueChange: (value: InclusionMode) => void; + label?: string; + }; + users: { + value: InclusionMode; + options: { id: InclusionMode; name: string }[]; + onValueChange: (value: InclusionMode) => void; + label?: string; + }; + orgUnits: { + value: InclusionMode; + options: { id: InclusionMode; name: string }[]; + onValueChange: (value: InclusionMode) => void; + label?: string; + }; +}; + +export const InclusionFields: React.FC = ({ sharingSettings, users, orgUnits }) => ( + <> + + + value={sharingSettings.value} + items={sharingSettings.options} + label={sharingSettings.label ?? i18n.t("Include owner and sharing settings")} + onValueChange={sharingSettings.onValueChange} + hideEmpty + /> + + + + value={users.value} + items={users.options} + label={users.label ?? i18n.t("Include users")} + onValueChange={users.onValueChange} + hideEmpty + /> + + + + value={orgUnits.value} + items={orgUnits.options} + label={orgUnits.label ?? i18n.t("Include organisation units")} + onValueChange={orgUnits.onValueChange} + hideEmpty + /> + + +); + +const DropdownContainer = styled.div` + margin-block-end: 16px; + + & > div { + width: 100%; + margin-block-start: 20px; + margin-block-end: 20px; + margin-inline-start: -10px; + } +`; diff --git a/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx b/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx index 63e488a89..ad07e2873 100644 --- a/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx @@ -6,7 +6,8 @@ import Dropdown from "../../dropdown/Dropdown"; import { Toggle } from "../../toggle/Toggle"; import { SyncWizardStepProps } from "../Steps"; import { styled } from "styled-components"; -import { IncludeObjectsAndReferences, useMetadataIncludeExcludeStep } from "./useMetadataIncludeExcludeStep"; +import { useMetadataIncludeExcludeStep } from "./useMetadataIncludeExcludeStep"; +import { InclusionFields } from "./InclusionFields"; const useStyles = makeStyles({ includeExcludeContainer: { @@ -63,38 +64,23 @@ const MetadataIncludeExcludeStep: React.FC = ({ syncRule, o return modelSelectItems.length > 0 ? (
- - - value={sharingSettingsObjectsAndReferencesValue} - items={includeObjectsAndReferencesOptions} - label={i18n.t("Include owner and sharing settings")} - style={{ width: "100%", marginTop: 20, marginBottom: 20, marginLeft: -10 }} - onValueChange={changeSharingSettingsObjectsAndReferences} - hideEmpty - /> - - - - - value={usersObjectsAndReferencesValue} - items={includeObjectsAndReferencesOptions} - label={i18n.t("Include users")} - style={{ width: "100%", marginTop: 20, marginBottom: 20, marginLeft: -10 }} - onValueChange={changeUsersObjectsAndReferences} - hideEmpty - /> - - - - - value={orgUnitsObjectsAndReferencesValue} - items={includeObjectsAndReferencesOptions} - label={i18n.t("Include organisation units")} - style={{ width: "100%", marginTop: 20, marginBottom: 20, marginLeft: -10 }} - onValueChange={changeOrgUnitsObjectsAndReferences} - hideEmpty - /> - + {syncRule.type === "metadata" && (
diff --git a/src/presentation/react/core/components/sync-wizard/metadata/useMetadataIncludeExcludeStep.ts b/src/presentation/react/core/components/sync-wizard/metadata/useMetadataIncludeExcludeStep.ts index 716786236..e54da6be7 100644 --- a/src/presentation/react/core/components/sync-wizard/metadata/useMetadataIncludeExcludeStep.ts +++ b/src/presentation/react/core/components/sync-wizard/metadata/useMetadataIncludeExcludeStep.ts @@ -10,19 +10,18 @@ import { D2Model } from "../../../../../../models/dhis/default"; import { defaultName, modelFactory } from "../../../../../../models/dhis/factory"; import { useAppContext } from "../../../contexts/AppContext"; import { DropdownOption } from "../../dropdown/Dropdown"; +import { InclusionMode } from "../../../../../../domain/user-settings/UserSettings"; -export const includeObjectsAndReferencesMap = { +export const includeObjectsAndReferencesMap: Record = { includeObjectsAndReferences: i18n.t("Include objects and references"), includeOnlyReferences: i18n.t("Include only references"), removeObjectsAndReferences: i18n.t("Remove objects and references"), } as const; -export type IncludeObjectsAndReferences = keyof typeof includeObjectsAndReferencesMap; - -export const includeObjectsAndReferencesOptions: { id: IncludeObjectsAndReferences; name: string }[] = Object.entries( +export const includeObjectsAndReferencesOptions: { id: InclusionMode; name: string }[] = Object.entries( includeObjectsAndReferencesMap ).map(([id, name]) => ({ - id: id as IncludeObjectsAndReferences, + id: id as InclusionMode, name, })); @@ -184,21 +183,21 @@ export function useMetadataIncludeExcludeStep( [includeReferencesAndObjectsRules, onChange, selectedType, syncRule] ); - const sharingSettingsObjectsAndReferencesValue: IncludeObjectsAndReferences = useMemo(() => { + const sharingSettingsObjectsAndReferencesValue: InclusionMode = useMemo(() => { return getObjectsAndReferencesValue( syncParams.includeSharingSettingsObjectsAndReferences, syncParams.includeOnlySharingSettingsReferences ); }, [syncParams.includeSharingSettingsObjectsAndReferences, syncParams.includeOnlySharingSettingsReferences]); - const usersObjectsAndReferencesValue: IncludeObjectsAndReferences = useMemo(() => { + const usersObjectsAndReferencesValue: InclusionMode = useMemo(() => { return getObjectsAndReferencesValue( syncParams.includeUsersObjectsAndReferences, syncParams.includeOnlyUsersReferences ); }, [syncParams.includeUsersObjectsAndReferences, syncParams.includeOnlyUsersReferences]); - const orgUnitsObjectsAndReferencesValue: IncludeObjectsAndReferences = useMemo(() => { + const orgUnitsObjectsAndReferencesValue: InclusionMode = useMemo(() => { return getObjectsAndReferencesValue( syncParams.includeOrgUnitsObjectsAndReferences, syncParams.includeOnlyOrgUnitsReferences @@ -206,7 +205,7 @@ export function useMetadataIncludeExcludeStep( }, [syncParams.includeOrgUnitsObjectsAndReferences, syncParams.includeOnlyOrgUnitsReferences]); const changeSharingSettingsObjectsAndReferences = useCallback( - (value: IncludeObjectsAndReferences) => { + (value: InclusionMode) => { onChange( syncRule.updateSyncParams({ ...syncRule.syncParams, @@ -219,7 +218,7 @@ export function useMetadataIncludeExcludeStep( ); const changeUsersObjectsAndReferences = useCallback( - (value: IncludeObjectsAndReferences) => { + (value: InclusionMode) => { onChange( syncRule.updateSyncParams({ ...syncRule.syncParams, @@ -232,7 +231,7 @@ export function useMetadataIncludeExcludeStep( ); const changeOrgUnitsObjectsAndReferences = useCallback( - (value: IncludeObjectsAndReferences) => { + (value: InclusionMode) => { onChange( syncRule.updateSyncParams({ ...syncRule.syncParams, @@ -330,7 +329,7 @@ function getModels(metadata: MetadataPackage, metadataModelsSync function getObjectsAndReferencesValue( includeObjectsAndReferences: boolean, includeOnlyReferences: boolean -): IncludeObjectsAndReferences { +): InclusionMode { if (includeObjectsAndReferences) { return "includeObjectsAndReferences"; } diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 08c8d985d..4e573843c 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -38,6 +38,7 @@ import { TestWrapper } from "../../../../react/core/components/test-wrapper/Test import { useAppContext } from "../../../../react/core/contexts/AppContext"; import InstancesSelectors from "./InstancesSelectors"; import { DataStoreMetadata } from "../../../../../domain/data-store/DataStoreMetadata"; +import { useUserSettings } from "../settings/useUserSettings"; const config: Record< SynchronizationType, @@ -87,8 +88,11 @@ const ManualSyncPage: React.FC = () => { const history = useHistory(); const { type } = useParams() as { type: SynchronizationType }; const { title, models } = config[type]; + const { userSettings } = useUserSettings(); - const [syncRule, updateSyncRule] = useState(SynchronizationRule.createOnDemand(type)); + const [syncRule, updateSyncRule] = useState( + SynchronizationRule.createOnDemand(type, userSettings.inclusionConfig) + ); const [appConfigurator, updateAppConfigurator] = useState(false); const [syncReport, setSyncReport] = useState(null); const [syncDialogOpen, setSyncDialogOpen] = useState(false); @@ -122,14 +126,14 @@ const ManualSyncPage: React.FC = () => { const updateSelection = useCallback( (selection: string[], exclusion: string[]) => { updateSyncRule(({ originInstance, targetInstances }) => - SynchronizationRule.createOnDemand(type) + SynchronizationRule.createOnDemand(type, userSettings.inclusionConfig) .updateBuilder({ originInstance }) .updateTargetInstances(targetInstances) .updateMetadataIds(selection) .updateExcludedIds(exclusion) ); }, - [type] + [type, userSettings] ); const openSynchronizationDialog = () => { diff --git a/src/presentation/webapp/core/pages/settings/SettingsPage.tsx b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx index 3f0d77f79..4e6bd777b 100644 --- a/src/presentation/webapp/core/pages/settings/SettingsPage.tsx +++ b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx @@ -7,6 +7,8 @@ import i18n from "../../../../../utils/i18n"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import { StorageSettingDropdown } from "./storage/StorageSettingDropdown"; import { useSettings } from "./useSettings"; +import { useUserSettings } from "./useUserSettings"; +import { InclusionFields } from "../../../../react/core/components/sync-wizard/metadata/InclusionFields"; export const SettingsPage: React.FC = () => { const history = useHistory(); @@ -28,6 +30,13 @@ export const SettingsPage: React.FC = () => { info, } = useSettings(); + const { userSettings, updateUserSettingsInclusionConfig, saveUserSettings, inclusionOptions } = useUserSettings(); + + const handleSave = useCallback(async () => { + await saveUserSettings(); + onSave(); + }, [onSave, saveUserSettings]); + const backHome = useCallback(() => history.push("/dashboard"), [history]); useEffect(() => { @@ -80,12 +89,37 @@ export const SettingsPage: React.FC = () => { onChange={onChangeRetentionDays} /> + +

{i18n.t("Metadata Sync User Settings")}

+ + updateUserSettingsInclusionConfig("sharing", value), + label: i18n.t("Default owner and sharing settings inclusion method"), + }} + users={{ + value: userSettings.inclusionConfig.users, + options: inclusionOptions, + onValueChange: value => updateUserSettingsInclusionConfig("users", value), + label: i18n.t("Default users inclusion method"), + }} + orgUnits={{ + value: userSettings.inclusionConfig.organisationUnits, + options: inclusionOptions, + onValueChange: value => updateUserSettingsInclusionConfig("organisationUnits", value), + label: i18n.t("Default organisation units inclusion method"), + }} + /> +
+ - @@ -98,6 +132,7 @@ export const SettingsPage: React.FC = () => { const useStyles = makeStyles({ content: { margin: "1rem", marginBottom: 35, marginLeft: 0 }, title: { marginTop: 0 }, + subsection: { marginTop: "1rem" }, container: { margin: "1rem", padding: "1rem" }, }); diff --git a/src/presentation/webapp/core/pages/settings/useUserSettings.ts b/src/presentation/webapp/core/pages/settings/useUserSettings.ts new file mode 100644 index 000000000..ec987b01f --- /dev/null +++ b/src/presentation/webapp/core/pages/settings/useUserSettings.ts @@ -0,0 +1,54 @@ +import React, { useCallback, useEffect } from "react"; + +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import { UserSettings } from "../../../../../domain/user-settings/UserSettings"; +import { useSnackbar } from "@eyeseetea/d2-ui-components"; +import { includeObjectsAndReferencesOptions } from "../../../../react/core/components/sync-wizard/metadata/useMetadataIncludeExcludeStep"; + +export function useUserSettings() { + const { newCompositionRoot } = useAppContext(); + const snackbar = useSnackbar(); + + const [userSettings, setUserSettings] = React.useState(UserSettings.default()); + + const updateUserSettingsInclusionConfig = useCallback( + (key: K, value: UserSettings["inclusionConfig"][K]) => { + if (!userSettings) return; + else { + setUserSettings(userSettings => userSettings?.updateInclusionConfig(key, value)); + } + }, + [userSettings] + ); + + const saveUserSettings = useCallback(() => { + if (!userSettings) return; + else { + return newCompositionRoot.userSettings.save + .execute(userSettings) + .toPromise() + .then(() => snackbar.info("User settings saved successfully!")) + .catch(error => { + console.error(`error saving user settings: ${error}`); + snackbar.error("Error saving user settings. Please try again later."); + }); + } + }, [newCompositionRoot, userSettings, snackbar]); + + useEffect(() => { + return newCompositionRoot.userSettings.get.execute().run( + userSettings => setUserSettings(userSettings), + error => { + console.error(`error fetching user settings: ${error}`); + snackbar.error("Error fetching user settings. Please try again later."); + } + ); + }, [newCompositionRoot, snackbar]); + + return { + userSettings, + updateUserSettingsInclusionConfig, + saveUserSettings, + inclusionOptions: includeObjectsAndReferencesOptions, + }; +} diff --git a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx index 6ae317a3d..3a89bd36b 100644 --- a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx @@ -8,6 +8,7 @@ import PageHeader from "../../../../react/core/components/page-header/PageHeader import SyncWizard from "../../../../react/core/components/sync-wizard/SyncWizard"; import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import { useUserSettings } from "../settings/useUserSettings"; export interface SyncRulesCreationParams { id: string; @@ -21,6 +22,7 @@ const SyncRulesCreation: React.FC = () => { const loading = useLoading(); const { id, action, type } = useParams() as SyncRulesCreationParams; const { compositionRoot } = useAppContext(); + const { userSettings } = useUserSettings(); const [dialogOpen, updateDialogOpen] = useState(false); const [syncRule, updateSyncRule] = useState(location.state?.syncRule ?? SynchronizationRule.create(type)); @@ -52,8 +54,11 @@ const SyncRulesCreation: React.FC = () => { setOriginalSyncRule(syncRule ?? SynchronizationRule.create(type)); loading.reset(); }); + } else { + updateSyncRule(syncRule => syncRule?.setDefaultInclusions(userSettings.inclusionConfig)); + setOriginalSyncRule(syncRule => syncRule?.setDefaultInclusions(userSettings.inclusionConfig)); } - }, [compositionRoot, loading, isEdit, id, type]); + }, [compositionRoot, loading, isEdit, id, type, userSettings.inclusionConfig]); return (