diff --git a/.eslintrc.json b/.eslintrc.json index 8968cf5e8f..e5d5724f64 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -147,6 +147,7 @@ ], "rules": { + "prefer-arrow-callback": "error", "react/function-component-definition": "off", "no-tabs": "off", "no-console":["error",{"allow":["warn","error"]}], diff --git a/hat/assets/js/apps/Iaso/constants/menu.tsx b/hat/assets/js/apps/Iaso/constants/menu.tsx index 5dbe5a4000..d99be26dcf 100644 --- a/hat/assets/js/apps/Iaso/constants/menu.tsx +++ b/hat/assets/js/apps/Iaso/constants/menu.tsx @@ -55,7 +55,7 @@ import { DropdownOptions } from '../types/utils'; import { PluginsContext } from '../utils'; import { useCurrentUser } from '../utils/usersUtils'; import MESSAGES from './messages'; -import { CHANGE_REQUEST, CONFIGURATION } from './urls'; +import { CHANGE_REQUEST, CHANGE_REQUEST_CONFIG, CONFIGURATION } from './urls'; // !! remove permission property if the menu has a subMenu !! const menuItems = ( @@ -206,7 +206,7 @@ const menuItems = ( }, { label: formatMessage(MESSAGES.configuration), - key: CHANGE_REQUEST, + key: CHANGE_REQUEST_CONFIG, icon: props => , subMenu: [ { diff --git a/hat/assets/js/apps/Iaso/constants/urls.ts b/hat/assets/js/apps/Iaso/constants/urls.ts index ab07a99a8a..d37a0c76cc 100644 --- a/hat/assets/js/apps/Iaso/constants/urls.ts +++ b/hat/assets/js/apps/Iaso/constants/urls.ts @@ -37,10 +37,12 @@ const orgUnitDetailsLogsParams = paginationPathParamsWithPrefix(LOGS_PREFIX); const orgUnitDetailsFormsParams = paginationPathParamsWithPrefix(FORMS_PREFIX); export const CHANGE_REQUEST = 'changeRequest'; +export const CHANGE_REQUEST_CONFIG = 'changeRequestConfig'; export const CONFIGURATION = 'configuration'; const ORG_UNITS = 'orgunits'; const ORG_UNITS_CHANGE_REQUEST = `${ORG_UNITS}/${CHANGE_REQUEST}`; -const ORG_UNITS_CONFIGURATION_CHANGE_REQUESTS = `${ORG_UNITS_CHANGE_REQUEST}/${CONFIGURATION}`; +const ORG_UNITS_CHANGE_REQUEST_CONFIG = `${ORG_UNITS}/${CHANGE_REQUEST_CONFIG}`; +const ORG_UNITS_CONFIGURATION_CHANGE_REQUESTS = `${ORG_UNITS_CHANGE_REQUEST_CONFIG}/${CONFIGURATION}`; // TODO export to blsq-comp export type RouteConfig = { @@ -195,21 +197,9 @@ export const baseRouteConfigs: Record = { url: ORG_UNITS_CONFIGURATION_CHANGE_REQUESTS, params: [ 'accountId', - 'parent_id', - 'groups', 'org_unit_type_id', - 'status', - 'created_at_after', - 'created_at_before', - 'forms', - 'userIds', - 'userRoles', - 'withLocation', - 'projectIds', - 'paymentStatus', + 'project_id', ...paginationPathParams, - 'paymentIds', - 'potentialPaymentIds', ], }, registry: { @@ -350,7 +340,13 @@ export const baseRouteConfigs: Record = { }, groupSets: { url: 'orgunits/groupSets', - params: ['accountId', 'search', 'sourceVersion', 'projectsIds', ...paginationPathParams], + params: [ + 'accountId', + 'search', + 'sourceVersion', + 'projectsIds', + ...paginationPathParams, + ], }, groupSetDetail: { url: 'orgunits/groupSet', diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/en.json b/hat/assets/js/apps/Iaso/domains/app/translations/en.json index 0f06304619..7642ff5d74 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/en.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/en.json @@ -320,8 +320,8 @@ "iaso.groups.sourceVersion": "Source version", "iaso.groups.update": "Update group", "iaso.groupsets.dialog.delete": "Are you sure you want to delete this groupset?", - "iaso.groupsets.dialog.deleteText":"This operation cannot be undone.", - "iaso.groupsets.groupBelonging": "Groups belonging", + "iaso.groupsets.dialog.deleteText": "This operation cannot be undone.", + "iaso.groupsets.groupBelonging": "Groups belonging", "iaso.groupsets.validation.field_required": "Ce champ est obligatoire", "iaso.hospital": "Hospital", "iaso.instance.coordinate": "Coordinates", @@ -595,6 +595,7 @@ "iaso.label.name": "Name", "iaso.label.needsAuthentication": "Authentification required", "iaso.label.newOrgUnit": "New org. unit", + "iaso.label.next": "Next", "iaso.label.no": "No", "iaso.label.noDifference": "No difference", "iaso.label.noGeographicalData": "Without geography", @@ -900,6 +901,16 @@ "iaso.orgUnitsTypes.projects": "Projects", "iaso.orgUnitsTypes.subTypesErrors": "A sub org unit type cannot be a parent too ({typeName})", "iaso.orgUnitsTypes.update": "Update org unit type", + "iaso.oucrc.closedDate": "Closing Date", + "iaso.oucrc.editableReferenceFormIds": "Editable Reference Forms", + "iaso.oucrc.groupSetIds": "Group Sets", + "iaso.oucrc.orgUnitsEditable": "Should OrgUnits of this type be editable?", + "iaso.oucrc.otherGroupIds": "Other Groups", + "iaso.oucrc.oucrcCreateModalTitle": "OrgUnit Change Request Configuration - Creation", + "iaso.oucrc.oucrcCreateModalTitle2": "OrgUnit Change Request Configuration - Creation 2nd step", + "iaso.oucrc.oucrcCreateUpdateModalTitle": "OrgUnit Change Request Configuration - Update", + "iaso.oucrc.possibleParentTypeIds": "Possible New Parent Types", + "iaso.oucrc.possibleTypeIds": "Possible New Types", "iaso.page.deleteError": "Error removing embedded link", "iaso.page.deleteSuccess": "Embedded link successfully removed", "iaso.page.viewpages": "View embedded link. {linebreak} New tab: ctrl + click", @@ -1179,6 +1190,7 @@ "iaso.snackBar.fetchFormsError": "An error occurred while fetching forms list", "iaso.snackBar.fetchFormVersionsError": "An error occurred while fetching form versions", "iaso.snackBar.fetchGroupsError": "An error occurred while fetching groups list", + "iaso.snackBar.fetchGroupSetsError": "An error occurred while fetching group sets list", "iaso.snackBar.fetchingLogDetailError": "An error occurred while fetching log details", "iaso.snackBar.fetchInstanceDictError": "An error occurred while fetching instances list", "iaso.snackBar.fetchInstanceError": "An error occurred while fetching instance detail", @@ -1498,4 +1510,4 @@ "trypelim.permissions.zones": "Zones", "trypelim.permissions.zones_edit": "Edit zones", "trypelim.permissions.zones_shapes_edit": "Edit zone shapes" -} +} \ No newline at end of file diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json index d34dd1389c..597a89aa15 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json @@ -595,6 +595,7 @@ "iaso.label.name": "Nom", "iaso.label.needsAuthentication": "Authentification requise", "iaso.label.newOrgUnit": "Nouvelle unité d'org.", + "iaso.label.next": "Suivant", "iaso.label.no": "Non", "iaso.label.noDifference": "Aucune différence", "iaso.label.noGeographicalData": "Sans données géographiques", @@ -900,6 +901,16 @@ "iaso.orgUnitsTypes.projects": "Projets", "iaso.orgUnitsTypes.subTypesErrors": "Un sous type d'unité d'organisation ne peut pas aussi être parent ({typeName})", "iaso.orgUnitsTypes.update": "Mettre à jour le type d'unité d'organisation", + "iaso.oucrc.closedDate": "Date de fermeture", + "iaso.oucrc.editableReferenceFormIds": "Formulaires de référence modifiables", + "iaso.oucrc.groupSetIds": "Ensembles de groupes", + "iaso.oucrc.orgUnitsEditable": "Les unités d'organisation de ce type doivent-elles être modifiables ?", + "iaso.oucrc.otherGroupIds": "Autres groupes", + "iaso.oucrc.oucrcCreateModalTitle": "Configuration de demande de changement d'unité d'organisation - Création", + "iaso.oucrc.oucrcCreateModalTitle2": "Configuration de demande de changement d'unité d'organisation - Deuxième étape de création", + "iaso.oucrc.oucrcCreateUpdateModalTitle": "Configuration de demande de changement d'unité d'organisation - Mise à jour", + "iaso.oucrc.possibleParentTypeIds": "Types de parents possibles", + "iaso.oucrc.possibleTypeIds": "Nouveaux types possibles", "iaso.page.deleteError": "Erreur lors de la suppression du lien intégré", "iaso.page.deleteSuccess": "Lien intégré supprimée", "iaso.page.viewpages": "Voir le lien intégré. {linebreak} Nouvel onglet: ctrl + click", @@ -1179,6 +1190,7 @@ "iaso.snackBar.fetchFormsError": "Une erreur est survenue en récupérant la liste des formulaires", "iaso.snackBar.fetchFormVersionsError": "Une erreur est survenue en récupérant les version du formulaire", "iaso.snackBar.fetchGroupsError": "Une erreur est survenue en récupérant la liste des groupes", + "iaso.snackBar.fetchGroupSetsError": "Une erreur est survenue en récupérant la liste des group sets", "iaso.snackBar.fetchingLogDetailError": "Une erreur est survenu en récupérant l'historique", "iaso.snackBar.fetchInstanceDictError": "Une erreur est survenue en récupérant la liste des soumissions", "iaso.snackBar.fetchInstanceError": "Une erreur est survenue en récupérant le detail de la soumission", diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/ConfirmDeleteModal.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/ConfirmDeleteModal.tsx index 275f6f9dfb..1a12c67253 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/ConfirmDeleteModal.tsx +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/ConfirmDeleteModal.tsx @@ -6,15 +6,15 @@ import { useSafeIntl, } from 'bluesquare-components'; -import MESSAGES from '../messages'; -import { OrgUnitChangeRequestConfig } from '../types'; import { DeleteIconButton } from '../../../../components/Buttons/DeleteIconButton'; import { useDeleteOrgUnitChangeRequestConfig } from '../hooks/api/useDeleteOrgUnitChangeRequestConfig'; +import MESSAGES from '../messages'; +import { OrgUnitChangeRequestConfigurationFull } from '../types'; type Props = { isOpen: boolean; closeDialog: () => void; - config: OrgUnitChangeRequestConfig; + config: OrgUnitChangeRequestConfigurationFull; }; const ConfirmDeleteModal: FunctionComponent = ({ diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialog.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialog.tsx new file mode 100644 index 0000000000..05555477c9 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialog.tsx @@ -0,0 +1,272 @@ +import { Typography } from '@mui/material'; +import { + ConfirmCancelModal, + LoadingSpinner, + makeFullModal, + useSafeIntl, +} from 'bluesquare-components'; +import { useFormik } from 'formik'; +import { isEqual } from 'lodash'; +import React, { FunctionComponent, useCallback, useEffect } from 'react'; +import { EditIconButton } from '../../../../components/Buttons/EditIconButton'; +import InputComponent from '../../../../components/forms/InputComponent'; +import { useTranslatedErrors } from '../../../../libs/validation'; +import { useGetGroupDropdown } from '../../hooks/requests/useGetGroups'; +import { useGetGroupSetsDropdown } from '../../hooks/requests/useGetGroupSets'; +import { useGetOrgUnitTypesDropdownOptions } from '../../orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions'; +import { editableFieldsManyToManyFields } from '../constants'; +import { useGetFormDropdownOptions } from '../hooks/api/useGetFormDropdownOptions'; +import { useRetrieveOrgUnitChangeRequestConfig } from '../hooks/api/useRetrieveOrgUnitChangeRequestConfig'; +import { useSaveOrgUnitChangeRequestConfiguration } from '../hooks/api/useSaveOrgUnitChangeRequestConfiguration'; +import { useOrgUnitsEditableFieldsOptions } from '../hooks/useOrgUnitEditableFieldsOptions'; +import { useOrgUnitsEditableOptions } from '../hooks/useOrgUnitsEditableOptions'; +import { useValidationSchemaOUCRC } from '../hooks/useValidationSchemaOUCRC'; +import MESSAGES from '../messages'; +import { + OrgUnitChangeRequestConfiguration, + OrgUnitChangeRequestConfigurationForm, +} from '../types'; + +type Props = { + config: OrgUnitChangeRequestConfiguration; + isOpen: boolean; + closeDialog: () => void; +}; + +// we should filter forms, groups, groupsets, types using correct project id +/// marquer les types comme requis + +const OrgUnitChangeRequestConfigDialog: FunctionComponent = ({ + config, + isOpen, + closeDialog, +}) => { + const configValidationSchema = useValidationSchemaOUCRC(); + const { + values, + setFieldValue, + isValid, + handleSubmit, + isSubmitting, + errors, + touched, + setFieldTouched, + setValues, + } = useFormik({ + initialValues: { + projectId: config.project.id, + orgUnitTypeId: config.orgUnitType.id, + orgUnitsEditable: undefined, + editableFields: undefined, + possibleTypeIds: undefined, + possibleParentTypeIds: undefined, + groupSetIds: undefined, + editableReferenceFormIds: undefined, + otherGroupIds: undefined, + }, + validationSchema: configValidationSchema, + onSubmit: (newValues: OrgUnitChangeRequestConfigurationForm) => { + saveConfig({ + configId: config.id, + data: newValues, + }); + closeDialog(); + }, + }); + const { data: fetchedConfig, isLoading: isLoadingFullConfig } = + useRetrieveOrgUnitChangeRequestConfig(config?.id); + useEffect(() => { + if (fetchedConfig) { + setValues(fetchedConfig); + } + }, [fetchedConfig, setValues]); + const { data: orgUnitTypeOptions } = useGetOrgUnitTypesDropdownOptions( + config.project.id, + ); + const { data: groupOptions } = useGetGroupDropdown({ + defaultVersion: 'true', + }); + const { data: formOptions } = useGetFormDropdownOptions( + config.orgUnitType.id, + config.project.id, + ); + const { data: groupSetOptions } = useGetGroupSetsDropdown(); + const { mutateAsync: saveConfig } = + useSaveOrgUnitChangeRequestConfiguration(); + const orgUnitsEditableOptions = useOrgUnitsEditableOptions(); + const orgUnitEditableFieldsOptions = useOrgUnitsEditableFieldsOptions(); + const { formatMessage } = useSafeIntl(); + const getErrors = useTranslatedErrors({ + errors, + touched, + formatMessage, + messages: MESSAGES, + }); + + const onChange = useCallback( + (keyValue, value) => { + setFieldTouched(keyValue, true); + setFieldValue(keyValue, value); + }, + [setFieldValue, setFieldTouched], + ); + + const onChangeEditableFields = useCallback( + (keyValue, value) => { + // if a many-to-many field has some value, but the field is removed from editableFields, we need to clean the field + if (value) { + const split = value.split(','); + editableFieldsManyToManyFields.forEach(field => { + if (!split.includes(field)) { + setFieldValue(field, undefined); + } + }); + } + onChange(keyValue, value); + }, + [onChange, setFieldValue], + ); + + const onChangeOrgUnitsEditable = useCallback( + (keyValue, value) => { + // if we say that the org units are no longer editable, we need to clean everything up + const boolValue = value === 'true'; + if (!boolValue) { + editableFieldsManyToManyFields.forEach(field => { + setFieldValue(field, undefined); + }); + setFieldValue('editableFields', undefined); + setFieldValue('groupSetIds', undefined); + } + onChange(keyValue, boolValue); + }, + [onChange, setFieldValue], + ); + const allowConfirm = isValid && !isSubmitting && !isEqual(touched, {}); + return ( + null} + id="oucrcDialogCreate" + dataTestId="add-org-unit-config-button" + titleMessage={ + config?.id + ? formatMessage(MESSAGES.oucrcCreateUpdateModalTitle) + : formatMessage(MESSAGES.oucrcCreateSecondStepModalTitle) + } + closeDialog={closeDialog} + maxWidth="sm" + allowConfirm={allowConfirm} + cancelMessage={MESSAGES.cancel} + confirmMessage={ + config?.id + ? MESSAGES.oucrcModalUpdateButton + : MESSAGES.oucrcModalCreateButton + } + onConfirm={() => handleSubmit()} + onCancel={() => { + closeDialog(); + }} + > + {isLoadingFullConfig && } + + {formatMessage(MESSAGES.project)}: {config.project.name} + + + {formatMessage(MESSAGES.orgUnitType)}: {config.orgUnitType.name} + + + {values?.orgUnitsEditable && ( + + )} + {values?.orgUnitsEditable && ( + + )} + {values?.editableFields?.includes('possibleTypeIds') && ( + + )} + {values?.editableFields?.includes('possibleParentTypeIds') && ( + + )} + {values?.editableFields?.includes('editableReferenceFormIds') && ( + + )} + {values?.editableFields?.includes('otherGroupIds') && ( + + )} + + ); +}; + +const modalWithButton = makeFullModal( + OrgUnitChangeRequestConfigDialog, + EditIconButton, +); + +export { + OrgUnitChangeRequestConfigDialog as OrgUnitChangeRequestConfigDialogCreateSecondStep, + modalWithButton as OrgUnitChangeRequestConfigDialogUpdate +}; + diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialogCreateFirstStep.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialogCreateFirstStep.tsx new file mode 100644 index 0000000000..e6c6923849 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Dialog/OrgUnitChangeRequestConfigDialogCreateFirstStep.tsx @@ -0,0 +1,151 @@ +import { + AddButton, + ConfirmCancelModal, + makeFullModal, + useSafeIntl, +} from 'bluesquare-components'; +import { useFormik } from 'formik'; +import { isEqual } from 'lodash'; +import React, { FunctionComponent, useCallback } from 'react'; +import * as Yup from 'yup'; +import InputComponent from '../../../../components/forms/InputComponent'; +import { useTranslatedErrors } from '../../../../libs/validation'; +import { useGetProjectsDropdownOptions } from '../../../projects/hooks/requests'; +import { useGetOUCRCCheckAvailabilityDropdownOptions } from '../hooks/api/useGetOUCRCCheckAvailabilityDropdownOptions'; +import MESSAGES from '../messages'; + +type Props = { + isOpen: boolean; + closeDialog: () => void; + // eslint-disable-next-line no-unused-vars + openCreationSecondStepDialog: (config: object) => void; +}; + +const useCreationSchema = () => { + const { formatMessage } = useSafeIntl(); + return Yup.object().shape({ + projectId: Yup.string() + .nullable() + .required(formatMessage(MESSAGES.requiredField)), + orgUnitTypeId: Yup.string() + .nullable() + .required(formatMessage(MESSAGES.requiredField)), + }); +}; + +const OrgUnitChangeRequestConfigDialogCreateFirstStep: FunctionComponent< + Props +> = ({ isOpen, closeDialog, openCreationSecondStepDialog }) => { + const creationSchema = useCreationSchema(); + const { + values, + setFieldValue, + isValid, + handleSubmit, + isSubmitting, + errors, + touched, + setFieldTouched, + } = useFormik({ + initialValues: { + projectId: undefined, + orgUnitTypeId: undefined, + }, + validationSchema: creationSchema, + onSubmit: () => { + const projectOption = allProjects?.find( + project => `${project.value}` === `${values.projectId}`, + ); + const orgUnitTypeOption = orgUnitTypeOptions?.find( + orgUnitType => + `${orgUnitType.value}` === `${values.orgUnitTypeId}`, + ); + openCreationSecondStepDialog({ + project: projectOption + ? { + id: projectOption.value, + name: projectOption.label, + } + : undefined, + orgUnitType: orgUnitTypeOption + ? { + id: orgUnitTypeOption.value, + name: orgUnitTypeOption.label, + } + : undefined, + }); + }, + }); + + const { formatMessage } = useSafeIntl(); + const getErrors = useTranslatedErrors({ + errors, + touched, + formatMessage, + messages: MESSAGES, + }); + + const { data: allProjects, isFetching: isFetchingProjects } = + useGetProjectsDropdownOptions(false); + const { data: orgUnitTypeOptions, isFetching: isFetchingOrgUnitTypes } = + useGetOUCRCCheckAvailabilityDropdownOptions(values.projectId); + + const onChange = useCallback( + (keyValue, value) => { + setFieldTouched(keyValue, true); + setFieldValue(keyValue, value); + }, + [setFieldValue, setFieldTouched], + ); + + const allowConfirm = isValid && !isSubmitting && !isEqual(touched, {}); + + return ( + null} + id="oucrcDialogCreate" + dataTestId="add-org-unit-config-button" + titleMessage={formatMessage(MESSAGES.oucrcCreateModalTitle)} + closeDialog={closeDialog} + maxWidth="xs" + allowConfirm={allowConfirm} + cancelMessage={MESSAGES.cancel} + confirmMessage={MESSAGES.next} + onConfirm={() => handleSubmit()} + onCancel={() => { + closeDialog(); + }} + > + + + + ); +}; + +const modalWithButton = makeFullModal( + OrgUnitChangeRequestConfigDialogCreateFirstStep, + AddButton, +); + +export { modalWithButton as OrgUnitChangeRequestConfigDialogCreateFirstStep }; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Filter/OrgUnitChangeRequestConfigsFilter.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Filter/OrgUnitChangeRequestConfigsFilter.tsx index eb2ef4532e..917d4d85c0 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Filter/OrgUnitChangeRequestConfigsFilter.tsx +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Filter/OrgUnitChangeRequestConfigsFilter.tsx @@ -1,14 +1,14 @@ -import React, { FunctionComponent } from 'react'; import { Box, Grid } from '@mui/material'; import { useSafeIntl } from 'bluesquare-components'; -import { useFilterState } from '../../../../hooks/useFilterState'; +import React, { FunctionComponent } from 'react'; +import { FilterButton } from '../../../../components/FilterButton'; import InputComponent from '../../../../components/forms/InputComponent'; import { baseUrls } from '../../../../constants/urls'; -import { useGetOrgUnitTypesDropdownOptions } from '../../orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions'; -import { OrgUnitChangeRequestConfigsParams } from '../types'; +import { useFilterState } from '../../../../hooks/useFilterState'; import { useGetProjectsDropdownOptions } from '../../../projects/hooks/requests'; +import { useGetOrgUnitTypesDropdownOptions } from '../../orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions'; import MESSAGES from '../messages'; -import { FilterButton } from '../../../../components/FilterButton'; +import { OrgUnitChangeRequestConfigsParams } from '../types'; const baseUrl = baseUrls.orgUnitsChangeRequestConfiguration; type Props = { params: OrgUnitChangeRequestConfigsParams }; @@ -30,7 +30,7 @@ export const OrgUnitChangeRequestConfigsFilter: FunctionComponent = ({ = ({ labelString={formatMessage(MESSAGES.orgUnitType)} /> - - + + ({ ...commonStyles(theme), @@ -19,12 +24,23 @@ export const OrgUnitChangeRequestConfigs: FunctionComponent = () => { const params = useParamsObject( baseUrls.orgUnitsChangeRequestConfiguration, ) as unknown as OrgUnitChangeRequestConfigsParams; + const { data, isFetching } = useGetOrgUnitChangeRequestConfigs(params); + const [isCreationSecondStepDialogOpen, setIsCreationSecondStepDialogOpen] = + useState(false); + const [config, setConfig] = useState(); + + const handleSecondStep = useCallback( + newConfig => { + setConfig(newConfig); + setIsCreationSecondStepDialogOpen(true); + }, + [setIsCreationSecondStepDialogOpen, setConfig], + ); const classes: Record = useStyles(); const { formatMessage } = useSafeIntl(); - return (
{ - { - // TODO - }} + + {isCreationSecondStepDialogOpen && config && ( + { + setIsCreationSecondStepDialogOpen(false); + }} + config={config} + /> + )} { - // TODO - }} + onEditClicked={handleSecondStep} />
diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/EditableFieldsCell.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/EditableFieldsCell.tsx new file mode 100644 index 0000000000..4f086fb45c --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/EditableFieldsCell.tsx @@ -0,0 +1,38 @@ +/* eslint-disable camelcase */ +import { Box, Chip } from '@mui/material'; +import { textPlaceholder, useSafeIntl } from 'bluesquare-components'; +import React, { useMemo } from 'react'; +import { computeEditableFields } from '../hooks/api/useRetrieveOrgUnitChangeRequestConfig'; +import MESSAGES from '../messages'; +import { OrgUnitChangeRequestConfigurationFull } from '../types'; + +export const EditableFieldsCell = ({ + row: { original }, +}: { + row: { original: OrgUnitChangeRequestConfigurationFull }; +}) => { + const { formatMessage } = useSafeIntl(); + const editableFields = useMemo( + () => computeEditableFields(original), + [original], + ); + + if (editableFields.length === 0) { + return textPlaceholder; + } + + return ( + + {editableFields.map(field => ( + + ))} + + ); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/OrgUnitChangeRequestConfigsTable.tsx b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/OrgUnitChangeRequestConfigsTable.tsx index 4e48abb72b..2f34275268 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/OrgUnitChangeRequestConfigsTable.tsx +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/Tables/OrgUnitChangeRequestConfigsTable.tsx @@ -1,25 +1,26 @@ +import { Column, useSafeIntl } from 'bluesquare-components'; import React, { - FunctionComponent, Dispatch, + FunctionComponent, SetStateAction, - useMemo, useCallback, + useMemo, } from 'react'; -import { Column, useSafeIntl } from 'bluesquare-components'; +import { EditIconButton } from '../../../../components/Buttons/EditIconButton'; +import { DateTimeCell } from '../../../../components/Cells/DateTimeCell'; import { TableWithDeepLink } from '../../../../components/tables/TableWithDeepLink'; import { baseUrls } from '../../../../constants/urls'; +import { ConfirmDeleteModal } from '../Dialog/ConfirmDeleteModal'; +import MESSAGES from '../messages'; import { OrgUnitChangeRequestConfigsPaginated, OrgUnitChangeRequestConfigsParams, - OrgUnitChangeRequestConfig, + OrgUnitChangeRequestConfiguration, } from '../types'; -import MESSAGES from '../messages'; -import { DateTimeCell } from '../../../../components/Cells/DateTimeCell'; -import { EditIconButton } from '../../../../components/Buttons/EditIconButton'; -import { ConfirmDeleteModal } from '../Dialog/ConfirmDeleteModal'; +import { EditableFieldsCell } from './EditableFieldsCell'; const useColumns = ( - onEditClicked: Dispatch>, + onEditClicked: Dispatch>, ): Column[] => { const { formatMessage } = useSafeIntl(); // @ts-ignore @@ -56,8 +57,8 @@ const useColumns = ( { Header: formatMessage(MESSAGES.editable_fields), id: 'editable_fields', - accessor: row => row.editable_fields.join(', '), - width: 600, + sortable: false, + Cell: EditableFieldsCell, }, { Header: formatMessage(MESSAGES.actions), @@ -66,7 +67,12 @@ const useColumns = ( sortable: false, Cell: settings => { const handleEdit = useCallback(() => { - onEditClicked(settings.row.original); + const configToUpdate = { + id: settings.row.original.id, + project: settings.row.original.project, + orgUnitType: settings.row.original.org_unit_type, + }; + onEditClicked(configToUpdate); }, [settings.row.original]); return ( <> @@ -87,7 +93,7 @@ const useColumns = ( type Props = { data: OrgUnitChangeRequestConfigsPaginated | undefined; isFetching: boolean; - onEditClicked: Dispatch>; + onEditClicked: Dispatch>; params: OrgUnitChangeRequestConfigsParams; }; export const baseUrl = baseUrls.orgUnitsChangeRequestConfiguration; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/constants.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/constants.ts index c04885c24e..eda4c27b07 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/constants.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/constants.ts @@ -1 +1,31 @@ -export const apiUrl = '/api/orgunits/changes/configs/'; +export const apiUrlOUCRC = '/api/orgunits/changes/configs/'; +export const apiUrlOUCRCCheckAvailability = `${apiUrlOUCRC}check_availability/`; +export const apiUrlForms = '/api/forms/'; +export const editableFields = [ + 'name', + // 'aliases', commented out because right now the feature is not ready yet + 'openingDate', + 'closedDate', + 'location', + 'possibleTypeIds', + 'possibleParentTypeIds', + 'editableReferenceFormIds', + 'otherGroupIds', +]; +export const mappingEditableFieldsForBackend = { + name: 'name', + // 'aliases': 'aliases', commented out because right now the feature is not ready yet + openingDate: 'opening_date', + closedDate: 'closing_date', + location: 'location', + possibleTypeIds: 'org_unit_type', + possibleParentTypeIds: 'parent_type', + editableReferenceFormIds: 'editable_reference_forms', + otherGroupIds: 'other_groups', +}; +export const editableFieldsManyToManyFields = [ + 'possibleTypeIds', + 'possibleParentTypeIds', + 'editableReferenceFormIds', + 'otherGroupIds', +]; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useDeleteOrgUnitChangeRequestConfig.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useDeleteOrgUnitChangeRequestConfig.ts index f1834fa0b0..5df2bfdbdf 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useDeleteOrgUnitChangeRequestConfig.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useDeleteOrgUnitChangeRequestConfig.ts @@ -1,16 +1,22 @@ import { UseMutationResult } from 'react-query'; import { useSnackMutation } from '../../../../../libs/apiHooks'; -import { apiUrl } from '../../constants'; -import { OrgUnitChangeRequestConfig } from '../../types'; import { deleteRequest } from '../../../../../libs/Api'; +import { apiUrlOUCRC } from '../../constants'; +import { OrgUnitChangeRequestConfigurationFull } from '../../types'; -const deleteOrgUnitChangeRequestConfigs = (config: OrgUnitChangeRequestConfig) => { - return deleteRequest(`${apiUrl}/${config.id}/`) as Promise; +const deleteOrgUnitChangeRequestConfigs = ( + config: OrgUnitChangeRequestConfigurationFull, +) => { + return deleteRequest(`${apiUrlOUCRC}${config.id}/`) as Promise; }; export const useDeleteOrgUnitChangeRequestConfig = (): UseMutationResult => useSnackMutation({ mutationFn: deleteOrgUnitChangeRequestConfigs, - invalidateQueryKey: 'getOrgUnitChangeRequestConfigs', + invalidateQueryKey: [ + 'useRetrieveOrgUnitChangeRequestConfig', + 'getOrgUnitChangeRequestConfigs', + 'checkAvailabilityOrgUnitChangeRequestConfigs', + ], }); diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetFormDropdownOptions.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetFormDropdownOptions.ts new file mode 100644 index 0000000000..989f84cbd5 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetFormDropdownOptions.ts @@ -0,0 +1,35 @@ +import { UseQueryResult } from 'react-query'; + +import { useSnackQuery } from '../../../../../libs/apiHooks'; +import { getRequest } from '../../../../../libs/Api'; + +import { Form } from '../../../../forms/types/forms'; +import { DropdownOptions } from '../../../../../types/utils'; +import { apiUrlForms } from '../../constants'; + +export const useGetFormDropdownOptions = ( + orgUnitTypeId: number, +): UseQueryResult[], Error> => { + const url = `${apiUrlForms}?orgUnitTypeIds=${orgUnitTypeId}`; + return useSnackQuery({ + queryKey: ['useGetFormDropdownOptions', url], + queryFn: () => getRequest(url), + options: { + enabled: Boolean(orgUnitTypeId), + staleTime: 1000 * 60 * 15, // in MS + cacheTime: 1000 * 60 * 5, + keepPreviousData: true, + retry: false, + select: data => { + return ( + data?.forms?.map((form: Form) => { + return { + value: form.id, + label: form.name, + }; + }) ?? [] + ); + }, + }, + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOUCRCCheckAvailabilityDropdownOptions.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOUCRCCheckAvailabilityDropdownOptions.ts new file mode 100644 index 0000000000..64e62d32c0 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOUCRCCheckAvailabilityDropdownOptions.ts @@ -0,0 +1,32 @@ +import { UseQueryResult } from 'react-query'; +import { getRequest } from '../../../../../libs/Api'; +import { useSnackQuery } from '../../../../../libs/apiHooks'; + +import { apiUrlOUCRCCheckAvailability } from '../../constants'; +import { OrgUnitType } from '../../types'; +import { DropdownOptions } from '../../../../../types/utils'; + +export const useGetOUCRCCheckAvailabilityDropdownOptions = ( + projectId?: number, +): UseQueryResult[], Error> => { + const url = `${apiUrlOUCRCCheckAvailability}?project_id=${projectId}`; + return useSnackQuery({ + queryKey: ['checkAvailabilityOrgUnitChangeRequestConfigs', url], + queryFn: () => getRequest(url), + options: { + enabled: Boolean(projectId), + staleTime: 1000 * 60 * 15, // in MS + cacheTime: 1000 * 60 * 5, + keepPreviousData: true, + retry: false, + select: data => { + return ( + data?.map((orgUnitType: OrgUnitType) => ({ + value: orgUnitType.id, + label: orgUnitType.name, + })) ?? [] + ); + }, + }, + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOrgUnitChangeRequestConfigs.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOrgUnitChangeRequestConfigs.ts index e45fbcf509..5cea58eff7 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOrgUnitChangeRequestConfigs.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useGetOrgUnitChangeRequestConfigs.ts @@ -4,7 +4,7 @@ import { getRequest } from '../../../../../libs/Api'; import { useSnackQuery } from '../../../../../libs/apiHooks'; import { useLocale } from '../../../../app/contexts/LocaleContext'; -import { apiUrl } from '../../constants'; +import { apiUrlOUCRC } from '../../constants'; import { OrgUnitChangeRequestConfigsPaginated, OrgUnitChangeRequestConfigsParams, @@ -26,7 +26,7 @@ export const useGetOrgUnitChangeRequestConfigs = ( page: params.page, }; - const url = makeUrlWithParams(apiUrl, apiParams); + const url = makeUrlWithParams(apiUrlOUCRC, apiParams); return useSnackQuery({ // Including locale in the query key because we need to make a call to update translations coming from the backend queryKey: ['getOrgUnitChangeRequestConfigs', url, locale], diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useRetrieveOrgUnitChangeRequestConfig.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useRetrieveOrgUnitChangeRequestConfig.ts new file mode 100644 index 0000000000..eacaf808e0 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useRetrieveOrgUnitChangeRequestConfig.ts @@ -0,0 +1,65 @@ +import { UseQueryResult } from 'react-query'; +import { getRequest } from '../../../../../libs/Api'; +import { useSnackQuery } from '../../../../../libs/apiHooks'; + +import { apiUrlOUCRC } from '../../constants'; +import { + OrgUnitChangeRequestConfigurationForm, + OrgUnitChangeRequestConfigurationFull, +} from '../../types'; + +const retrieveOrgUnitChangeRequestConfig = (url: string) => { + return getRequest(url) as Promise; +}; + +const fieldMapping: { [key: string]: string } = { + opening_date: 'openingDate', + closing_date: 'closedDate', + org_unit_type: 'possibleTypeIds', + parent_type: 'possibleParentTypeIds', + editable_reference_forms: 'editableReferenceFormIds', + other_groups: 'otherGroupIds', +}; + +export const computeEditableFields = ( + data: OrgUnitChangeRequestConfigurationFull, +): string[] => { + return (data.editable_fields || []).map(field => { + return fieldMapping[field] || field; + }); +}; + +const mapAndJoin = (items: any[] = []) => items?.map(item => item.id).join(','); + +export const useRetrieveOrgUnitChangeRequestConfig = ( + configId?: number, +): UseQueryResult => { + const url = `${apiUrlOUCRC}${configId}/`; + return useSnackQuery({ + queryKey: ['useRetrieveOrgUnitChangeRequestConfig', configId], + queryFn: () => retrieveOrgUnitChangeRequestConfig(url), + options: { + enabled: Boolean(configId), + staleTime: 1000 * 60 * 15, // in MS + cacheTime: 1000 * 60 * 5, + keepPreviousData: true, + select: (data: OrgUnitChangeRequestConfigurationFull) => { + return { + projectId: data.project.id, + orgUnitTypeId: data.org_unit_type.id, + orgUnitsEditable: data.org_units_editable, + editableFields: computeEditableFields(data).join(','), + possibleTypeIds: mapAndJoin(data.possible_types), + possibleParentTypeIds: mapAndJoin( + data.possible_parent_types, + ), + groupSetIds: mapAndJoin(data.group_sets), + editableReferenceFormIds: mapAndJoin( + data.editable_reference_forms, + ), + otherGroupIds: mapAndJoin(data.other_groups), + }; + }, + }, + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useSaveOrgUnitChangeRequestConfiguration.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useSaveOrgUnitChangeRequestConfiguration.ts new file mode 100644 index 0000000000..8e786a8eb6 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/api/useSaveOrgUnitChangeRequestConfiguration.ts @@ -0,0 +1,99 @@ +import { UseMutationResult } from 'react-query'; +import { patchRequest, postRequest } from '../../../../../libs/Api'; +import { useSnackMutation } from '../../../../../libs/apiHooks'; + +import { apiUrlOUCRC, mappingEditableFieldsForBackend } from '../../constants'; +import { OrgUnitChangeRequestConfigurationForm } from '../../types'; + +const cleanEditableFieldsForSaving = (editableFields?: string): string[] => { + if (!editableFields) { + return []; + } + return editableFields.split(',').map(field => { + return mappingEditableFieldsForBackend[field]; + }); +}; + +// All the many to many fields are added if they have a value +const splitAndMapToNumbers = (str?: string) => { + return str?.trim() ? str.split(',').map(Number) : []; +}; + +type ApiValues = { + org_units_editable: boolean; + project_id?: number; + org_unit_type_id?: number; + editable_fields?: string[]; + possible_type_ids?: number[]; + possible_parent_type_ids?: number[]; + group_set_ids?: number[]; + editable_reference_form_ids?: number[]; + other_group_ids?: number[]; +}; + +const mapValuesForSaving = ( + configId: number | undefined, + values: OrgUnitChangeRequestConfigurationForm, +): ApiValues => { + const apiValues: ApiValues = { + org_units_editable: values.orgUnitsEditable ?? false, + }; + // These two fields can't be updated so they are only set for creation + if (!configId) { + apiValues.project_id = values.projectId; + apiValues.org_unit_type_id = values.orgUnitTypeId; + } + + // This field must be cleaned because the backend accepts only some values + apiValues.editable_fields = cleanEditableFieldsForSaving( + values.editableFields, + ); + + apiValues.possible_type_ids = splitAndMapToNumbers(values.possibleTypeIds); + apiValues.possible_parent_type_ids = splitAndMapToNumbers( + values.possibleParentTypeIds, + ); + apiValues.group_set_ids = splitAndMapToNumbers(values.groupSetIds); + apiValues.editable_reference_form_ids = splitAndMapToNumbers( + values.editableReferenceFormIds, + ); + apiValues.other_group_ids = splitAndMapToNumbers(values.otherGroupIds); + return apiValues; +}; + +const patchOUCRC = async (configId: number, body: ApiValues): Promise => { + const url = `${apiUrlOUCRC}${configId}/`; + return patchRequest(url, body); +}; + +const postOUCRC = async (body: ApiValues): Promise => { + return postRequest({ + url: `${apiUrlOUCRC}`, + data: body, + }); +}; + +export const useSaveOrgUnitChangeRequestConfiguration = + (): UseMutationResult => { + const ignoreErrorCodes = [400]; + return useSnackMutation({ + mutationFn: ({ + configId, + data, + }: { + configId: number | undefined; + data: OrgUnitChangeRequestConfigurationForm; + }) => { + const formattedData = mapValuesForSaving(configId, data); + return configId + ? patchOUCRC(configId, formattedData) + : postOUCRC(formattedData); + }, + ignoreErrorCodes, + invalidateQueryKey: [ + 'useRetrieveOrgUnitChangeRequestConfig', + 'getOrgUnitChangeRequestConfigs', + 'checkAvailabilityOrgUnitChangeRequestConfigs', + ], + }); + }; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitEditableFieldsOptions.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitEditableFieldsOptions.ts new file mode 100644 index 0000000000..9c55b5f8b4 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitEditableFieldsOptions.ts @@ -0,0 +1,15 @@ +import { useSafeIntl } from 'bluesquare-components'; +import { editableFields } from '../constants'; +import MESSAGES from '../messages'; +import { DropdownOptions } from '../../../../types/utils'; + +export const useOrgUnitsEditableFieldsOptions = + (): DropdownOptions[] => { + const { formatMessage } = useSafeIntl(); + return editableFields.map(field => { + return { + value: field, + label: formatMessage(MESSAGES[field]), + }; + }); + }; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitsEditableOptions.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitsEditableOptions.ts new file mode 100644 index 0000000000..73ba060397 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useOrgUnitsEditableOptions.ts @@ -0,0 +1,17 @@ +import { useSafeIntl } from 'bluesquare-components'; +import { DropdownOptions } from '../../../../types/utils'; +import MESSAGES from '../messages'; + +export const useOrgUnitsEditableOptions = (): DropdownOptions[] => { + const { formatMessage } = useSafeIntl(); + return [ + { + label: formatMessage(MESSAGES.orgUnitsEditableYes), + value: true, + }, + { + label: formatMessage(MESSAGES.orgUnitsEditableNo), + value: false, + }, + ]; +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useValidationSchemaOUCRC.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useValidationSchemaOUCRC.ts new file mode 100644 index 0000000000..c8fc7b4309 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/hooks/useValidationSchemaOUCRC.ts @@ -0,0 +1,73 @@ +import { useSafeIntl } from 'bluesquare-components'; +import * as yup from 'yup'; +import MESSAGES from '../messages'; + +yup.addMethod( + yup.string, + 'isMultiSelectValid', + function isMultiSelectValid(formatMessage) { + return this.test('isMultiSelectValid', '', (value, context) => { + const { path, createError, parent } = context; + if (!parent.editableFields && !parent.groupSetIds) { + return createError({ + path, + message: formatMessage(MESSAGES.requiredField), + }); + } + const splitFields = parent.editableFields || []; + const isFieldPopulated = parent[path]; + if (!isFieldPopulated && splitFields.includes(path)) { + return createError({ + path, + message: formatMessage(MESSAGES.requiredField), + }); + } + return true; + }); + }, +); + +export const useValidationSchemaOUCRC = () => { + const { formatMessage } = useSafeIntl(); + return yup.object().shape({ + projectId: yup + .string() + .nullable() + .required(formatMessage(MESSAGES.requiredField)), + orgUnitTypeId: yup + .string() + .nullable() + .required(formatMessage(MESSAGES.requiredField)), + orgUnitsEditable: yup + .boolean() + .nullable() + .required(formatMessage(MESSAGES.requiredField)), + editableFields: yup.string().nullable(), + possibleTypeIds: yup + .string() + .nullable() + .when('orgUnitsEditable', { + is: true, + then: yup.string().nullable().isMultiSelectValid(formatMessage), + otherwise: yup.string().nullable(), + }), + possibleParentTypeIds: yup + .string() + .nullable() + .when('orgUnitsEditable', { + is: true, + then: yup.string().nullable().isMultiSelectValid(formatMessage), + otherwise: yup.string().nullable(), + }), + groupSetIds: yup + .string() + .nullable() + .when('orgUnitsEditable', { + is: true, + then: yup.string().nullable().isMultiSelectValid(formatMessage), + otherwise: yup.string().nullable(), + }), + editableReferenceFormIds: yup.string().nullable(), + otherGroupIds: yup.string().nullable(), + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/messages.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/messages.ts index 54d4417198..25531bf043 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/messages.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/messages.ts @@ -61,6 +61,95 @@ const MESSAGES = defineMessages({ id: 'iaso.label.updated_by', defaultMessage: 'Updated by', }, + close: { + id: 'iaso.label.close', + defaultMessage: 'Close', + }, + next: { + id: 'iaso.label.next', + defaultMessage: 'Next', + }, + oucrcCreateModalTitle: { + id: 'iaso.oucrc.oucrcCreateModalTitle', + defaultMessage: 'OrgUnit Change Request Configuration - Creation', + }, + oucrcCreateSecondStepModalTitle: { + id: 'iaso.oucrc.oucrcCreateModalTitle2', + defaultMessage: + 'OrgUnit Change Request Configuration - Creation 2nd step', + }, + oucrcCreateUpdateModalTitle: { + id: 'iaso.oucrc.oucrcCreateUpdateModalTitle', + defaultMessage: 'OrgUnit Change Request Configuration - Update', + }, + requiredField: { + id: 'iaso.forms.error.fieldRequired', + defaultMessage: 'This field is required', + }, + orgUnitsEditable: { + id: 'iaso.oucrc.orgUnitsEditable', + defaultMessage: 'Should OrgUnits of this type be editable?', + }, + orgUnitsEditableYes: { + id: 'iaso.forms.yes', + defaultMessage: 'Yes', + }, + orgUnitsEditableNo: { + id: 'iaso.forms.no', + defaultMessage: 'No', + }, + name: { + id: 'iaso.label.name', + defaultMessage: 'Name', + }, + aliases: { + id: 'iaso.forms.aliases', + defaultMessage: 'Aliases', + }, + openingDate: { + id: 'iaso.changeRequest.openingDate', + defaultMessage: 'Opening Date', + }, + closedDate: { + id: 'iaso.oucrc.closedDate', + defaultMessage: 'Closing Date', + }, + location: { + id: 'iaso.label.location', + defaultMessage: 'Location', + }, + editableFields: { + id: 'iaso.label.editableFields', + defaultMessage: 'Editable Fields', + }, + possibleTypeIds: { + id: 'iaso.oucrc.possibleTypeIds', + defaultMessage: 'Possible New Types', + }, + possibleParentTypeIds: { + id: 'iaso.oucrc.possibleParentTypeIds', + defaultMessage: 'Possible New Parent Types', + }, + groupSetIds: { + id: 'iaso.oucrc.groupSetIds', + defaultMessage: 'Group Sets', + }, + editableReferenceFormIds: { + id: 'iaso.oucrc.editableReferenceFormIds', + defaultMessage: 'Editable Reference Forms', + }, + otherGroupIds: { + id: 'iaso.oucrc.otherGroupIds', + defaultMessage: 'Other Groups', + }, + oucrcModalCreateButton: { + id: 'iaso.label.create', + defaultMessage: 'Create', + }, + oucrcModalUpdateButton: { + id: 'iaso.mappings.label.update', + defaultMessage: 'Update', + }, }); export default MESSAGES; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/types.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/types.ts index 257efe5710..0b8e5a697b 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/types.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/configuration/types.ts @@ -15,9 +15,25 @@ export type OrgUnitType = { id: number; name: string; }; + +export type GroupSet = { + id: number; + name: string; +}; + +export type Form = { + id: number; + name: string; +}; + +export type Group = { + id: number; + name: string; +}; + export type NestedUser = Partial; -export type OrgUnitChangeRequestConfig = { +export type OrgUnitChangeRequestConfigListElement = { id: number; project: Project; org_unit_type: OrgUnitType; @@ -29,6 +45,41 @@ export type OrgUnitChangeRequestConfig = { updated_at: number; }; -export interface OrgUnitChangeRequestConfigsPaginated extends Pagination { - results: OrgUnitChangeRequestConfig[]; +export type OrgUnitChangeRequestConfigurationFull = { + id: number; + project: Project; + org_unit_type: OrgUnitType; + org_units_editable?: boolean; + editable_fields?: string[]; + possible_types?: Array; + possible_parent_types?: Array; + group_sets?: Array; + editable_reference_forms?: Array
; + other_groups?: Array; +}; + +export type OrgUnitChangeRequestConfigurationForm = { + projectId: number; + orgUnitTypeId: number; + orgUnitsEditable?: boolean; + editableFields?: string; + possibleTypeIds?: string; + possibleParentTypeIds?: string; + groupSetIds?: string; + editableReferenceFormIds?: string; + otherGroupIds?: string; }; + +export type OrgUnitChangeRequestConfiguration = { + id?: number; + project: Project; + orgUnitType: OrgUnitType; +}; + +export interface OrgUnitChangeRequestConfigsPaginated extends Pagination { + results: OrgUnitChangeRequestConfigListElement[]; +} + +export interface CheckAvailiabilityOrgUnitRequestConfig { + results: OrgUnitType[]; +} diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroupSets.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroupSets.ts new file mode 100644 index 0000000000..30098799c4 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroupSets.ts @@ -0,0 +1,33 @@ +import { UseQueryResult } from 'react-query'; +// @ts-ignore +import { useSnackQuery } from 'Iaso/libs/apiHooks.ts'; +// @ts-ignore +import { getRequest } from 'Iaso/libs/Api'; +import { DropdownOptions } from '../../../../types/utils'; + +import { staleTime } from '../../config'; +import MESSAGES from '../../messages'; + +export const useGetGroupSetsDropdown = (): UseQueryResult< + DropdownOptions[], + Error +> => { + return useSnackQuery({ + queryKey: ['groupSets'], + queryFn: () => getRequest(`/api/group_sets/dropdown/`), + snackErrorMsg: MESSAGES.fetchGroupSetsError, + options: { + staleTime, + select: data => { + if (!data) return []; + return data.map(groupSet => { + return { + value: groupSet.id, + label: groupSet.name, + original: groupSet, + }; + }); + }, + }, + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroups.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroups.ts index cb2d887d8d..ff19297f42 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroups.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetGroups.ts @@ -5,8 +5,8 @@ import { useSnackQuery } from 'Iaso/libs/apiHooks.ts'; import { getRequest } from 'Iaso/libs/Api'; import { DropdownOptions } from '../../../../types/utils'; -import MESSAGES from '../../messages'; import { staleTime } from '../../config'; +import MESSAGES from '../../messages'; type Props = { dataSourceId?: number; @@ -25,7 +25,6 @@ const makeGroupsQueryParams = ({ return ''; }; - export const useGetGroups = ({ dataSourceId, sourceVersionId, @@ -74,6 +73,7 @@ type Params = { sourceVersionId?: number; blockOfCountries?: string; appId?: string; + defaultVersion?: string; }; export const useGetGroupDropdown = ( params: Params, @@ -83,7 +83,7 @@ export const useGetGroupDropdown = ( if (params[keyInJS]) { queryParams[keyInApi] = params[keyInJS]; } - }); + }); const urlSearchParams = new URLSearchParams(queryParams); const queryString = urlSearchParams.toString(); return useSnackQuery({ diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/messages.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/messages.ts index 1645bdad5f..4220a87c78 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/messages.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/messages.ts @@ -382,6 +382,10 @@ const MESSAGES = defineMessages({ id: 'iaso.snackBar.fetchGroupsError', defaultMessage: 'An error occurred while fetching groups list', }, + fetchGroupSetsError: { + id: 'iaso.snackBar.fetchGroupSetsError', + defaultMessage: 'An error occurred while fetching group sets list', + }, fetchProfilesError: { id: 'iaso.snackBar.fetchProfilesError', defaultMessage: 'An error occurred while fetching profiles list', diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions.ts index c6476a5702..4190b98f87 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/orgUnitTypes/hooks/useGetOrgUnitTypesDropdownOptions.ts @@ -7,33 +7,41 @@ import { useSnackQuery } from '../../../../libs/apiHooks'; import { DropdownOptions } from '../../../../types/utils'; import { OrgunitTypesApi } from '../../types/orgunitTypes'; -const getOrgunitTypes = (): Promise => { - return getRequest('/api/v2/orgunittypes/'); +const getOrgunitTypes = (projectId?: number): Promise => { + return getRequest( + projectId + ? `/api/v2/orgunittypes/?project=${projectId}` + : '/api/v2/orgunittypes/', + ); }; -export const useGetOrgUnitTypesDropdownOptions = (): UseQueryResult< - DropdownOptions[], - Error -> => { - const queryKey: any[] = ['orgunittypes-dropdown']; - return useSnackQuery(queryKey, () => getOrgunitTypes(), undefined, { - keepPreviousData: true, - staleTime: 1000 * 60 * 15, // in MS - cacheTime: 1000 * 60 * 5, - select: data => { - if (!data) return []; - return data.orgUnitTypes - .sort((orgunitType1, orgunitType2) => { - const depth1 = orgunitType1.depth ?? 0; - const depth2 = orgunitType2.depth ?? 0; - return depth1 < depth2 ? -1 : 1; - }) - .map(orgunitType => { - return { - value: orgunitType.id.toString(), - label: orgunitType.name, - }; - }); +export const useGetOrgUnitTypesDropdownOptions = ( + projectId?: number, +): UseQueryResult[], Error> => { + const queryKey: any[] = ['orgunittypes-dropdown', projectId]; + return useSnackQuery( + queryKey, + () => getOrgunitTypes(projectId), + undefined, + { + keepPreviousData: true, + staleTime: 1000 * 60 * 15, // in MS + cacheTime: 1000 * 60 * 5, + select: data => { + if (!data) return []; + return data.orgUnitTypes + .sort((orgunitType1, orgunitType2) => { + const depth1 = orgunitType1.depth ?? 0; + const depth2 = orgunitType2.depth ?? 0; + return depth1 < depth2 ? -1 : 1; + }) + .map(orgunitType => { + return { + value: orgunitType.id.toString(), + label: orgunitType.name, + }; + }); + }, }, - }); + ); }; diff --git a/hat/assets/js/apps/Iaso/domains/projects/hooks/requests.ts b/hat/assets/js/apps/Iaso/domains/projects/hooks/requests.ts index d0ef3de92e..8086803715 100644 --- a/hat/assets/js/apps/Iaso/domains/projects/hooks/requests.ts +++ b/hat/assets/js/apps/Iaso/domains/projects/hooks/requests.ts @@ -1,12 +1,12 @@ -import { UseQueryResult, UseMutationResult, useQueryClient } from 'react-query'; -import { UrlParams, ApiParams } from 'bluesquare-components'; +import { ApiParams, UrlParams } from 'bluesquare-components'; +import { UseMutationResult, useQueryClient, UseQueryResult } from 'react-query'; import { getRequest, postRequest, putRequest } from '../../../libs/Api'; -import { useSnackQuery, useSnackMutation } from '../../../libs/apiHooks'; +import { useSnackMutation, useSnackQuery } from '../../../libs/apiHooks'; +import { DropdownOptions } from '../../../types/utils'; +import { FeatureFlag } from '../types/featureFlag'; import { PaginatedProjects } from '../types/paginatedProjects'; import { Project } from '../types/project'; -import { FeatureFlag } from '../types/featureFlag'; -import { DropdownOptions } from '../../../types/utils'; type ProjectApi = { projects: Array; @@ -15,10 +15,9 @@ const getProjects = (): Promise => { return getRequest('/api/projects/'); }; -export const useGetProjectsDropdownOptions = (): UseQueryResult< - DropdownOptions[], - Error -> => { +export const useGetProjectsDropdownOptions = ( + asString = true, +): UseQueryResult[], Error> => { const queryClient = useQueryClient(); const queryKey: any[] = ['projects-dropdown']; return useSnackQuery(queryKey, () => getProjects(), undefined, { @@ -32,7 +31,7 @@ export const useGetProjectsDropdownOptions = (): UseQueryResult< if (!data) return []; return data.projects.map(project => { return { - value: project.id.toString(), + value: asString ? project.id?.toString() : project.id, label: project.name, }; }); diff --git a/hat/assets/js/apps/Iaso/types/yup.d.ts b/hat/assets/js/apps/Iaso/types/yup.d.ts new file mode 100644 index 0000000000..fd68e3b2d3 --- /dev/null +++ b/hat/assets/js/apps/Iaso/types/yup.d.ts @@ -0,0 +1,9 @@ +import 'yup'; + +declare module 'yup' { + interface StringSchema { + isMultiSelectValid( + formatMessage: (message: any) => string, + ): StringSchema; + } +} diff --git a/hat/assets/js/cypress/integration/05 - orgUnits/changesConfiguration/changeRequestConfiguration.spec.js b/hat/assets/js/cypress/integration/05 - orgUnits/changesConfiguration/changeRequestConfiguration.spec.js index 043a9cddfb..1ec6c88628 100644 --- a/hat/assets/js/cypress/integration/05 - orgUnits/changesConfiguration/changeRequestConfiguration.spec.js +++ b/hat/assets/js/cypress/integration/05 - orgUnits/changesConfiguration/changeRequestConfiguration.spec.js @@ -4,15 +4,15 @@ import emptyFixture from '../../../fixtures/orgunits/changes/configuration/empty import page2 from '../../../fixtures/orgunits/changes/configuration/orgUnitChangeConfigurations-page2.json'; import listFixture from '../../../fixtures/orgunits/changes/configuration/orgUnitChangeConfigurations.json'; import orgUnitTypesFixture from '../../../fixtures/orgunittypes/list.json'; -import projectsFixture from '../../../fixtures/projects/list.json'; import superUser from '../../../fixtures/profiles/me/superuser.json'; +import projectsFixture from '../../../fixtures/projects/list.json'; import { testPageFilters } from '../../../support/testPageFilters'; import { testPagination } from '../../../support/testPagination'; import { testTablerender } from '../../../support/testTableRender'; import { testTableSort } from '../../../support/testTableSort'; const siteBaseUrl = Cypress.env('siteBaseUrl'); -const baseUrl = `${siteBaseUrl}/dashboard/orgunits/configuration/changeRequest`; +const baseUrl = `${siteBaseUrl}/dashboard/orgunits/changeRequestConfig/configuration`; let interceptFlag = false; const defaultQuery = { @@ -85,7 +85,12 @@ const testRowContent = (index, config = listFixture.results[index]) => { r.colAt(2).should('contain', config.org_unit_type.name); r.colAt(3).should('contain', formatDate(config.created_at)); r.colAt(4).should('contain', formatDate(config.updated_at)); - r.colAt(5).should('contain', config.editable_fields.join(', ')); + r.colAt(5) + .find('.MuiChip-label') + .should(labels => { + const labelTexts = [...labels].map(label => label.textContent); + expect(labelTexts).to.include.members(['1', '2', '3']); + }); }); }; @@ -197,10 +202,6 @@ describe('OrgUnit Change Configuration', () => { colIndex: 4, order: 'updated_at', }, - { - colIndex: 5, - order: 'editable_fields', - }, ]; sorts.forEach(s => { testTableSort({ diff --git a/iaso/api/group_sets/views.py b/iaso/api/group_sets/views.py index 4f971df976..c60baa368e 100644 --- a/iaso/api/group_sets/views.py +++ b/iaso/api/group_sets/views.py @@ -1,13 +1,15 @@ import django_filters -from rest_framework import permissions +from rest_framework import filters, permissions, serializers, status +from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework import filters, status -from iaso.models import GroupSet -from ..common import ModelViewSet, HasPermission + from hat.menupermissions import models as permission from iaso.api.common import Paginator -from .serializers import GroupSetSerializer +from iaso.models import GroupSet, Project, SourceVersion + +from ..common import HasPermission, ModelViewSet from .filters import GroupSetFilter +from .serializers import GroupSetSerializer class HasGroupsetPermission(permissions.BasePermission): @@ -26,6 +28,13 @@ class GroupSetPagination(Paginator): page_size = 10 +class GroupSetDropdownSerializer(serializers.ModelSerializer): + class Meta: + model = GroupSet + fields = ["id", "name"] + read_only_fields = ["id", "name"] + + class GroupSetsViewSet(ModelViewSet): f"""Groups API @@ -75,3 +84,35 @@ def create(self, request, *args, **kwargs): serializer.save() headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + @action(permission_classes=[], detail=False, methods=["GET"], serializer_class=GroupSetDropdownSerializer) + def dropdown(self, request, *args): + """To be used in dropdowns (filters) + + * Read only + * Readable anonymously if feature flag on project allow them and an app_id parameter is passed + * No permission needed + """ + + app_id = self.request.query_params.get("app_id") + user = request.user + if user and user.is_anonymous and app_id is None: + raise serializers.ValidationError("Parameter app_id is missing") + + if user and user.is_authenticated: + account = user.iaso_profile.account + # Filter on version ids (linked to the account) + versions = SourceVersion.objects.filter(data_source__projects__account=account) + + else: + # this check if project need auth + try: + project = Project.objects.get_for_user_and_app_id(user, app_id) + except Project.DoesNotExist: + raise serializers.ValidationError("No project found for the given app_id") + versions = SourceVersion.objects.filter(data_source__projects=project) + group_sets = GroupSet.objects.filter(source_version__in=versions).distinct() + + queryset = self.filter_queryset(group_sets) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) diff --git a/iaso/api/org_unit_change_request_configurations/serializers.py b/iaso/api/org_unit_change_request_configurations/serializers.py index 7e65f5f6c4..b76857fc9a 100644 --- a/iaso/api/org_unit_change_request_configurations/serializers.py +++ b/iaso/api/org_unit_change_request_configurations/serializers.py @@ -1,27 +1,20 @@ import django.core.serializers +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -from django.contrib.auth.models import User from hat.audit.audit_logger import AuditLogger from hat.audit.models import ORG_UNIT_CHANGE_REQUEST_CONFIGURATION_API +from iaso.api.common import TimestampField from iaso.api.org_unit_change_request_configurations.validation import ( - validate_org_unit_types, - validate_group_sets, validate_forms, + validate_group_sets, validate_groups, + validate_org_unit_types, ) from iaso.api.query_params import PROJECT_ID -from iaso.models import ( - OrgUnitType, - OrgUnitChangeRequestConfiguration, - Project, - GroupSet, - Form, - Group, -) +from iaso.models import Form, Group, GroupSet, OrgUnitChangeRequestConfiguration, OrgUnitType, Project from iaso.utils.serializer.id_or_uuid_field import IdOrUuidRelatedField -from iaso.api.common import TimestampField class UserNestedSerializer(serializers.ModelSerializer): @@ -126,6 +119,12 @@ class Meta: "created_at", "updated_by", "updated_at", + "editable_fields", + "possible_types", + "possible_parent_types", + "group_sets", + "editable_reference_forms", + "other_groups", ] diff --git a/iaso/api/org_unit_change_request_configurations/views.py b/iaso/api/org_unit_change_request_configurations/views.py index e89ef0f8e9..f099826b34 100644 --- a/iaso/api/org_unit_change_request_configurations/views.py +++ b/iaso/api/org_unit_change_request_configurations/views.py @@ -1,22 +1,22 @@ import django_filters from django.db.models import Q -from rest_framework import viewsets, filters, serializers +from rest_framework import filters, serializers, viewsets from rest_framework.decorators import action from rest_framework.response import Response from iaso.api.org_unit_change_request_configurations.filters import OrgUnitChangeRequestConfigurationListFilter from iaso.api.org_unit_change_request_configurations.pagination import OrgUnitChangeRequestConfigurationPagination from iaso.api.org_unit_change_request_configurations.permissions import ( - HasOrgUnitsChangeRequestConfigurationReadPermission, HasOrgUnitsChangeRequestConfigurationFullPermission, + HasOrgUnitsChangeRequestConfigurationReadPermission, ) from iaso.api.org_unit_change_request_configurations.serializers import ( + OrgUnitChangeRequestConfigurationAuditLogger, OrgUnitChangeRequestConfigurationListSerializer, OrgUnitChangeRequestConfigurationRetrieveSerializer, - OrgUnitChangeRequestConfigurationWriteSerializer, OrgUnitChangeRequestConfigurationUpdateSerializer, + OrgUnitChangeRequestConfigurationWriteSerializer, OrgUnitTypeNestedSerializer, - OrgUnitChangeRequestConfigurationAuditLogger, ProjectIdSerializer, ) from iaso.models import OrgUnitChangeRequestConfiguration, OrgUnitType diff --git a/iaso/models/org_unit_change_request_configuration.py b/iaso/models/org_unit_change_request_configuration.py index 4f5768fbe2..2c0cab5934 100644 --- a/iaso/models/org_unit_change_request_configuration.py +++ b/iaso/models/org_unit_change_request_configuration.py @@ -1,5 +1,6 @@ import typing -from django.contrib.auth.models import User, AnonymousUser + +from django.contrib.auth.models import AnonymousUser, User from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models @@ -8,10 +9,10 @@ from iaso.models.entity import UserNotAuthError from iaso.utils.models.soft_deletable import ( - SoftDeletableModel, DefaultSoftDeletableManager, - OnlyDeletedSoftDeletableManager, IncludeDeletedSoftDeletableManager, + OnlyDeletedSoftDeletableManager, + SoftDeletableModel, ) @@ -77,14 +78,16 @@ class OrgUnitChangeRequestConfiguration(SoftDeletableModel): OrgUnitChangeRequestConfigurationQuerySet )() - # Only the non-relationship fields since an ID present in any relationship - # means that the field is editable by the user LIST_OF_POSSIBLE_EDITABLE_FIELDS = [ "name", "aliases", "opening_date", - "closed_date", + "closing_date", "location", + "org_unit_type", + "parent_type", + "editable_reference_forms", + "other_groups", ] # Used to easily create/update objects in serializers