diff --git a/i18n/en.pot b/i18n/en.pot index fcdcdc0c..17fadb7c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-10-23T11:07:53.225Z\n" -"PO-Revision-Date: 2024-10-23T11:07:53.225Z\n" +"POT-Creation-Date: 2024-11-05T07:23:15.731Z\n" +"PO-Revision-Date: 2024-11-05T07:23:15.732Z\n" msgid "schemas" msgstr "schemas" @@ -123,21 +123,6 @@ msgstr "Failed to load {{label}}" msgid "Failed to load" msgstr "Failed to load" -msgid "Download" -msgstr "Download" - -msgid "Merge" -msgstr "Merge" - -msgid "Delete source data element values" -msgstr "Delete source data element values" - -msgid "Last updated" -msgstr "Last updated" - -msgid "Discard" -msgstr "Discard" - msgid "Aggregation level(s)" msgstr "Aggregation level(s)" @@ -237,6 +222,9 @@ msgstr "Created" msgid "Last updated by" msgstr "Last updated by" +msgid "Last updated" +msgstr "Last updated" + msgid "Id" msgstr "Id" @@ -258,6 +246,9 @@ msgstr "Details" msgid "Failed to load details" msgstr "Failed to load details" +msgid "Download" +msgstr "Download" + msgid "Download {{section}}" msgstr "Download {{section}}" @@ -297,6 +288,9 @@ msgstr "Clear all filters" msgid "Category" msgstr "Category" +msgid "Category option" +msgstr "Category option" + msgid "Category option group" msgstr "Category option group" @@ -435,9 +429,6 @@ msgstr "Search for a user or group" msgid "Categories" msgstr "Categories" -msgid "Category option" -msgstr "Category option" - msgid "Category options" msgstr "Category options" @@ -828,6 +819,27 @@ msgstr "GeoJSON" msgid "Disaggregation" msgstr "Disaggregation" +msgid "Point" +msgstr "Point" + +msgid "MultiPoint" +msgstr "MultiPoint" + +msgid "LineString" +msgstr "LineString" + +msgid "MultiLineString" +msgstr "MultiLineString" + +msgid "Polygon" +msgstr "Polygon" + +msgid "MultiPolygon" +msgstr "MultiPolygon" + +msgid "GeometryCollection" +msgstr "GeometryCollection" + msgid "Aggregation type" msgstr "Aggregation type" @@ -855,6 +867,9 @@ msgstr "Zero is significant" msgid "Data dimension type" msgstr "Data dimension type" +msgid "Ignore data approval" +msgstr "Ignore data approval" + msgid "This field requires a unique value, please choose another one" msgstr "This field requires a unique value, please choose another one" @@ -964,12 +979,24 @@ msgid "Category option Groups" msgstr "Category option Groups" msgid "" -"Choose the category option Groups to include in this category option group " +"Choose the category option groups to include in this category option group " "set." msgstr "" -"Choose the category option Groups to include in this category option group " +"Choose the category option groups to include in this category option group " "set." +msgid "Available category option groups" +msgstr "Available category option groups" + +msgid "Selected category option groups" +msgstr "Selected category option groups" + +msgid "Filter available category option groups" +msgstr "Filter available category option groups" + +msgid "Filter selected category option groups" +msgstr "Filter selected category option groups" + msgid "Set up the basic information for this category option group." msgstr "Set up the basic information for this category option group." @@ -1127,6 +1154,12 @@ msgstr "" "included. PHU will still be available for the PHU level, but not included " "in the aggregations to the levels above." +msgid "Longitude" +msgstr "Longitude" + +msgid "{{type}} coordinates are not editable" +msgstr "{{type}} coordinates are not editable" + msgid "Upload an image" msgstr "Upload an image" @@ -1176,17 +1209,11 @@ msgstr "Location" msgid "Set up the organisation unit location." msgstr "Set up the organisation unit location." -msgid "Latitude" -msgstr "Latitude" - -msgid "Longitude" -msgstr "Longitude" - -msgid "Reference assignments" -msgstr "Reference assignments" +msgid "Reference assignment" +msgstr "Reference assignment" -msgid "Assign the organisation unit to related models." -msgstr "Assign the organisation unit to related models." +msgid "Assign the organisation unit to related objects." +msgstr "Assign the organisation unit to related objects." msgid "Available data sets" msgstr "Available data sets" @@ -1212,8 +1239,8 @@ msgstr "Filter available programs" msgid "Filter selected programs" msgstr "Filter selected programs" -msgid "New organisation unit will be created inside {{displayName}}" -msgstr "New organisation unit will be created inside {{displayName}}" +msgid "Organisation unit will be positioned inside {{displayName}}" +msgstr "Organisation unit will be positioned inside {{displayName}}" msgid "Creating first organisation unit" msgstr "Creating first organisation unit" diff --git a/src/components/form/DefaultFormErrorNotice.tsx b/src/components/form/DefaultFormErrorNotice.tsx index c5102749..9ba043bc 100644 --- a/src/components/form/DefaultFormErrorNotice.tsx +++ b/src/components/form/DefaultFormErrorNotice.tsx @@ -87,7 +87,7 @@ const ErrorList = ({ errors }: { errors: Record }) => { > {labels.get(key) || key}: - {value} + {JSON.stringify(value)} ) })} diff --git a/src/lib/constants/translatedModelConstants.ts b/src/lib/constants/translatedModelConstants.ts index e3ed1315..f46b332e 100644 --- a/src/lib/constants/translatedModelConstants.ts +++ b/src/lib/constants/translatedModelConstants.ts @@ -69,11 +69,22 @@ export const DATA_DIMENSION_TYPE = { ATTRIBUTE: i18n.t('Attribute'), } +export const GEOMETRY_TYPE = { + POINT: i18n.t('Point'), + MULTIPOINT: i18n.t('MultiPoint'), + LINESTRING: i18n.t('LineString'), + MULTILINESTRING: i18n.t('MultiLineString'), + POLYGON: i18n.t('Polygon'), + MULTIPOLYGON: i18n.t('MultiPolygon'), + GEOMETRYCOLLECTION: i18n.t('GeometryCollection'), +} + const allConstantTranslations: Record = { ...AGGREGATION_TYPE, ...DOMAIN_TYPE, ...VALUE_TYPE, ...DATA_DIMENSION_TYPE, + ...GEOMETRY_TYPE, } export const getConstantTranslation = (constant: string): string => { diff --git a/src/lib/form/createJsonPatchOperations.ts b/src/lib/form/createJsonPatchOperations.ts index 4c55b061..451f47cc 100644 --- a/src/lib/form/createJsonPatchOperations.ts +++ b/src/lib/form/createJsonPatchOperations.ts @@ -15,7 +15,8 @@ type PatchAttributeValue = { value: AttributeValue['value'] } -type ModelWithAttributeValues = IdentifiableObject & { +export type ModelWithAttributeValues = { + id?: string attributeValues?: PatchAttributeValue[] } diff --git a/src/lib/form/useOnSubmit.ts b/src/lib/form/useOnSubmit.ts index 1e3f80fd..67e7e476 100644 --- a/src/lib/form/useOnSubmit.ts +++ b/src/lib/form/useOnSubmit.ts @@ -6,7 +6,10 @@ import { useNavigate } from 'react-router-dom' import { ModelSection } from '../../types' import { IdentifiableObject } from '../../types/generated' import { getSectionPath, useNavigateWithSearchState } from '../routeUtils' -import { createJsonPatchOperations } from './createJsonPatchOperations' +import { + createJsonPatchOperations, + ModelWithAttributeValues, +} from './createJsonPatchOperations' import { useCreateModel } from './useCreateModel' import { usePatchModel } from './usePatchModel' @@ -57,9 +60,25 @@ export const useOnSubmitEdit = ({ ) } -export const useOnSubmitNew = ({ +export const defaultNewValueFormatter = < + TFormValues extends ModelWithAttributeValues +>( + values: TFormValues +) => { + if (values.attributeValues) { + return { + ...values, + attributeValues: values.attributeValues.filter( + ({ value }) => !!value + ), + } + } + return values +} + +export const useOnSubmitNew = ({ section, - valueFormatter, + valueFormatter = defaultNewValueFormatter, }: { section: ModelSection valueFormatter?: (values: TFormValues) => Record @@ -96,6 +115,6 @@ export const useOnSubmitNew = ({ }) navigate(`/${getSectionPath(section)}`) }, - [createModel, saveAlert, navigate, section] + [createModel, saveAlert, navigate, section, valueFormatter] ) } diff --git a/src/pages/organisationUnits/Edit.tsx b/src/pages/organisationUnits/Edit.tsx new file mode 100644 index 00000000..81b22b2b --- /dev/null +++ b/src/pages/organisationUnits/Edit.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { useQuery } from 'react-query' +import { useParams } from 'react-router-dom' +import { DefaultEditFormContents, FormBase } from '../../components' +import { + ATTRIBUTE_VALUES_FIELD_FILTERS, + DEFAULT_FIELD_FILTERS, + SECTIONS_MAP, + useOnSubmitEdit, + validate, +} from '../../lib' +import { useBoundResourceQueryFn } from '../../lib/query/useBoundQueryFn' +import { OrganisationUnit, PickWithFieldFilters } from '../../types/generated' +import { OrganisationUnitFormField, organisationUnitSchema } from './form' + +const fieldFilters = [ + ...DEFAULT_FIELD_FILTERS, + ...ATTRIBUTE_VALUES_FIELD_FILTERS, + 'name', + 'code', + 'shortName', + 'openingDate', + 'closedDate', + 'comment', + 'image[id,name]', + 'description', + 'contactPerson', + 'address', + 'email', + 'phoneNumber', + 'url', + 'geometry', + 'dataSets', + 'programs', + 'level', + 'path', + 'parent[id,path, displayName]', +] as const + +export type OrgUnitFormValues = PickWithFieldFilters< + OrganisationUnit, + typeof fieldFilters +> + +const section = SECTIONS_MAP.organisationUnit + +export const Component = () => { + const queryFn = useBoundResourceQueryFn() + const modelId = useParams().id as string + + const query = { + resource: 'organisationUnits', + id: modelId, + params: { + fields: fieldFilters.concat(), + }, + } + const orgUnit = useQuery({ + queryKey: [query], + queryFn: queryFn, + }) + + return ( + { + return validate(organisationUnitSchema, values) + }} + > + + + + + ) +} diff --git a/src/pages/organisationUnits/New.tsx b/src/pages/organisationUnits/New.tsx index d50a541a..95649477 100644 --- a/src/pages/organisationUnits/New.tsx +++ b/src/pages/organisationUnits/New.tsx @@ -9,24 +9,6 @@ import { organisationUnitSchema, } from './form' -const formatFormValues: (values: FormValues) => Record = ( - values -) => { - return { - ...values, - geometry: - values.geometry?.longitude && values.geometry?.latitude - ? { - type: 'Point', - coordinates: [ - values.geometry?.longitude, - values.geometry?.latitude, - ], - } - : undefined, - attributeValues: values.attributeValues.filter(({ value }) => !!value), - } -} const section = SECTIONS_MAP.organisationUnit export const Component = () => { @@ -34,7 +16,6 @@ export const Component = () => { { diff --git a/src/pages/organisationUnits/form/GeometryFields.tsx b/src/pages/organisationUnits/form/GeometryFields.tsx new file mode 100644 index 00000000..9598f932 --- /dev/null +++ b/src/pages/organisationUnits/form/GeometryFields.tsx @@ -0,0 +1,78 @@ +import i18n from '@dhis2/d2-i18n' +import { Field, InputField } from '@dhis2/ui' +import React from 'react' +import { useField } from 'react-final-form' +import { getConstantTranslation } from '../../../lib' + +export function GeometryFields() { + const fieldName = 'geometry' + const { input, meta } = useField(fieldName) + + const handleChange = ({ + longitude, + latitude, + }: { + longitude?: number + latitude?: number + }) => { + const geometry = { + type: 'Point', + coordinates: [longitude || undefined, latitude || undefined], + } + + input.onChange(geometry) + } + + return !input.value || input.value?.type === 'Point' ? ( + <> + + + handleChange({ + longitude: e.value + ? parseFloat(e.value) + : undefined, + latitude: input.value?.coordinates?.[1], + }) + } + label={i18n.t('Longitude')} + inputWidth="400px" + name="longitude" + type="number" + value={input.value.coordinates?.[0]?.toString()} + min="-90" + max="90" + step="any" + /> + + handleChange({ + longitude: input.value?.coordinates?.[0], + latitude: e.value ? parseFloat(e.value) : undefined, + }) + } + inputWidth="400px" + label={i18n.t('Longitude')} + name="latitude" + type="number" + value={input.value?.coordinates?.[1]?.toString()} + min="-180" + max="180" + step="any" + /> + + + ) : ( + + ) +} diff --git a/src/pages/organisationUnits/form/ImageField.module.css b/src/pages/organisationUnits/form/ImageField.module.css index c1be1b9f..821cd5d3 100644 --- a/src/pages/organisationUnits/form/ImageField.module.css +++ b/src/pages/organisationUnits/form/ImageField.module.css @@ -1,8 +1,6 @@ .fileInputWrapper { display: flex; flex-wrap: wrap; - flex-direction: column; - max-width: 400px; gap: var(--spacers-dp4); } diff --git a/src/pages/organisationUnits/form/ImageField.tsx b/src/pages/organisationUnits/form/ImageField.tsx index e130c283..9607af76 100644 --- a/src/pages/organisationUnits/form/ImageField.tsx +++ b/src/pages/organisationUnits/form/ImageField.tsx @@ -43,7 +43,7 @@ export function ImageField() { const postResponse = (await dataEngine.mutate({ resource: 'fileResources', type: 'create', - data: { file: fileToUpload }, + data: { file: fileToUpload, domain: 'ORG_UNIT' }, })) as { response: { fileResource: { id: string; storageStatus: string } @@ -103,7 +103,6 @@ export function ImageField() { valid={!!(input.value && input.value.id)} /> - {input.value?.id && ( } diff --git a/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx b/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx index 8a660411..49e6a074 100644 --- a/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx +++ b/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx @@ -13,9 +13,10 @@ import { import { DefaultIdentifiableFields, DescriptionField, -} from '../../../components/form' +} from '../../../components' import { DateField } from '../../../components/form/fields/DateField' import { SCHEMA_SECTIONS, useSystemSetting } from '../../../lib' +import { GeometryFields } from './GeometryFields' import { ImageField } from './ImageField' import { OrganisationUnitSelector } from './OrganisationUnitSelector' @@ -126,26 +127,7 @@ export function OrganisationUnitFormField() { {i18n.t('Set up the organisation unit location.')} - - - component={InputFieldFF} - inputWidth="400px" - label={i18n.t('Latitude')} - name="geometry.latitude" - type="number" - min="-90" - max="90" - /> - - - component={InputFieldFF} - inputWidth="400px" - label={i18n.t('Longitude')} - name="geometry.longitude" - type="number" - min="-180" - max="180" - /> + {allowReferenceAssignments && ( diff --git a/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx b/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx index 16ba244a..0d0172af 100644 --- a/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx +++ b/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import { Field, NoticeBox, OrganisationUnitTree } from '@dhis2/ui' import { IconInfo16 } from '@dhis2/ui-icons' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useField } from 'react-final-form' import { useCurrentUserRootOrgUnits } from '../../../lib/user/currentUserStore' import classes from './OrganisationUnitSelector.module.css' @@ -11,7 +11,9 @@ export function OrganisationUnitSelector() { const { input, meta } = useField(fieldName, { format: (value) => value }) const userRootOrgUnits = useCurrentUserRootOrgUnits() const userRootOrgUnitsIds = userRootOrgUnits.map((unit) => `/${unit.id}`) - const [selected, setSelected] = useState<[string] | []>([]) + const [selected, setSelected] = useState<[string] | []>( + input.value?.path ? [input.value.path] : [] + ) const handleChange = (orgUnit: { displayName: string @@ -40,7 +42,10 @@ export function OrganisationUnitSelector() { singleSelection roots={userRootOrgUnitsIds} selected={selected} - initiallyExpanded={userRootOrgUnitsIds} + initiallyExpanded={[ + ...userRootOrgUnitsIds, + ...selected, + ]} /> {input.value?.displayName && ( @@ -48,7 +53,7 @@ export function OrganisationUnitSelector() {

{i18n.t( - 'New organisation unit will be created inside {{displayName}}', + 'Organisation unit will be positioned inside {{displayName}}', { displayName: input.value.displayName } )}

diff --git a/src/pages/organisationUnits/form/organisationUnitSchema.ts b/src/pages/organisationUnits/form/organisationUnitSchema.ts index 320f4ba7..cf3f5af3 100644 --- a/src/pages/organisationUnits/form/organisationUnitSchema.ts +++ b/src/pages/organisationUnits/form/organisationUnitSchema.ts @@ -21,9 +21,21 @@ export const organisationUnitSchema = identifiable parent: z.object({ id: z.string() }).optional(), geometry: z .object({ - longitude: z.string().optional(), - latitude: z.string().optional(), + type: z.literal('Point'), + coordinates: z.array(z.number()).length(2), }) + .or( + z.object({ + type: z.union([ + z.literal('Multipoint'), + z.literal('Linestring'), + z.literal('Multilinestring'), + z.literal('Polygon'), + z.literal('Multipolygon'), + z.literal('Geometrycollection'), + ]), + }) + ) .optional(), programs: referenceCollection.optional().default([]), dataSets: referenceCollection.optional().default([]),