diff --git a/i18n/en.pot b/i18n/en.pot index 8e3d64b6..402fbf95 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-09-30T15:09:50.864Z\n" -"PO-Revision-Date: 2024-09-30T15:09:50.864Z\n" +"POT-Creation-Date: 2024-10-22T10:31:57.931Z\n" +"PO-Revision-Date: 2024-10-22T10:31:57.932Z\n" msgid "schemas" msgstr "schemas" @@ -108,6 +108,9 @@ msgstr "{{fieldLabel}} (required)" msgid "Name" msgstr "Name" +msgid "Select organisation units" +msgstr "Select organisation units" + msgid "Often used in reports where space is limited" msgstr "Often used in reports where space is limited" @@ -120,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)" @@ -234,6 +222,9 @@ msgstr "Created" msgid "Last updated by" msgstr "Last updated by" +msgid "Last updated" +msgstr "Last updated" + msgid "Id" msgstr "Id" @@ -255,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}}" @@ -294,6 +288,9 @@ msgstr "Clear all filters" msgid "Category" msgstr "Category" +msgid "Category option group" +msgstr "Category option group" + msgid "Type to filter options" msgstr "Type to filter options" @@ -447,9 +444,6 @@ msgstr "Category option combination" msgid "Category option combinations" msgstr "Category option combinations" -msgid "Category option group" -msgstr "Category option group" - msgid "Category option groups" msgstr "Category option groups" @@ -933,6 +927,25 @@ msgstr "Filter selected categories" msgid "At least one category is required" msgstr "At least one category is required" +msgid "Set up the basic information for this category option." +msgstr "Set up the basic information for this category option." + +msgid "An alternative name used in section or automatic data entry forms." +msgstr "An alternative name used in section or automatic data entry forms." + +msgid "Availability configuration" +msgstr "Availability configuration" + +msgid "" +"Choose when, and for which organisation units this category option will be " +"available." +msgstr "" +"Choose when, and for which organisation units this category option will be " +"available." + +msgid "End date should be after start date" +msgstr "End date should be after start date" + msgid "Create data element group" msgstr "Create data element group" @@ -991,9 +1004,6 @@ msgstr "Use a pattern to limit what information can be entered." msgid "e.g. 999-000-0000" msgstr "e.g. 999-000-0000" -msgid "An alternative name used in section or automatic data entry forms." -msgstr "An alternative name used in section or automatic data entry forms." - msgid "Selected legends" msgstr "Selected legends" @@ -1065,6 +1075,74 @@ msgstr "" "included. PHU will still be available for the PHU level, but not included " "in the aggregations to the levels above." +msgid "Upload an image" +msgstr "Upload an image" + +msgid "Remove" +msgstr "Remove" + +msgid "Max size 5MB. Supported file size are .jpg, .png, and .gif." +msgstr "Max size 5MB. Supported file size are .jpg, .png, and .gif." + +msgid "Preview of current icon" +msgstr "Preview of current icon" + +msgid "Placement in hierarchy" +msgstr "Placement in hierarchy" + +msgid "" +"Choose where this new organisation unit should be placed in the existing " +"hierarchy" +msgstr "" +"Choose where this new organisation unit should be placed in the existing " +"hierarchy" + +msgid "Set up the basic information for this organisation unit." +msgstr "Set up the basic information for this organisation unit." + +msgid "Opening date" +msgstr "Opening date" + +msgid "Closed date" +msgstr "Closed date" + +msgid "Comment" +msgstr "Comment" + +msgid "Contact person" +msgstr "Contact person" + +msgid "Address" +msgstr "Address" + +msgid "A web link that provides extra information." +msgstr "A web link that provides extra information." + +msgid "Location" +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 "New organisation unit will be created inside {{displayName}}" +msgstr "New organisation unit will be created inside {{displayName}}" + +msgid "Creating first organisation unit" +msgstr "Creating first organisation unit" + +msgid "" +"This is the first organisation unit and will be created as the root of the " +"hierarchy." +msgstr "" +"This is the first organisation unit and will be created as the root of the " +"hierarchy." + msgid "No organisation units available" msgstr "No organisation units available" diff --git a/src/components/form/attributes/CustomAttributes.tsx b/src/components/form/attributes/CustomAttributes.tsx index d897f869..8474b594 100644 --- a/src/components/form/attributes/CustomAttributes.tsx +++ b/src/components/form/attributes/CustomAttributes.tsx @@ -78,6 +78,19 @@ function CustomAttribute({ attribute, index }: CustomAttributeProps) { ) } + if (attribute.valueType === 'GEOJSON') { + return ( + + + + ) + } // @TODO: Verify that all value types have been covered! throw new Error(`Implement value type "${attribute.valueType}"!`) } diff --git a/src/components/form/fields/CodeField.tsx b/src/components/form/fields/CodeField.tsx index 60093076..01d2e69f 100644 --- a/src/components/form/fields/CodeField.tsx +++ b/src/components/form/fields/CodeField.tsx @@ -2,10 +2,11 @@ import i18n from '@dhis2/d2-i18n' import { InputFieldFF } from '@dhis2/ui' import React from 'react' import { Field as FieldRFF } from 'react-final-form' -import { SchemaSection, useCheckMaxLengthFromSchema } from '../../../lib' +import { SchemaSection } from '../../../lib' +import { useValidator } from '../../../lib/models/useFieldValidators' export function CodeField({ schemaSection }: { schemaSection: SchemaSection }) { - const validate = useCheckMaxLengthFromSchema(schemaSection.name, 'code') + const validator = useValidator({ schemaSection, property: 'code' }) return ( validator(code)} /> ) } diff --git a/src/components/form/fields/DateField.tsx b/src/components/form/fields/DateField.tsx index ed354125..5e42401c 100644 --- a/src/components/form/fields/DateField.tsx +++ b/src/components/form/fields/DateField.tsx @@ -10,10 +10,12 @@ type DateFieldProps = Omit< name: string // this is not exposed in CalendarInputProps - but it should be label?: string + required?: boolean } export function DateField({ name, label, + required, ...calendarInputProps }: DateFieldProps) { const calendar = useSystemSetting('keyCalendar') @@ -43,10 +45,10 @@ export function DateField({ timeZone={'utc'} locale={locale} error={meta.touched && meta.invalid && meta.error} - validationText={meta.touched && meta.error} + validationText={meta.touched ? meta.error : undefined} onBlur={(_, e) => input.onBlur(e)} clearable - label={label} + label={required ? `${label} *` : label} {...calendarInputProps} /> diff --git a/src/components/form/fields/OrganisationUnitField.tsx b/src/components/form/fields/OrganisationUnitField.tsx index c1d105e4..c65bff26 100644 --- a/src/components/form/fields/OrganisationUnitField.tsx +++ b/src/components/form/fields/OrganisationUnitField.tsx @@ -10,20 +10,28 @@ import { useCurrentUserRootOrgUnits } from '../../../lib/user/currentUserStore' type OrganisationUnitFieldProps = { name?: string + singleSelection?: boolean + onChange?: (orgUnits: OrganisationUnitFormValue[]) => void } -type OrganisationUnitFormValue = { +export type OrganisationUnitFormValue = { id: string path: string displayName: string } -export const OrganisationUnitField = ({ name }: OrganisationUnitFieldProps) => { +export const OrganisationUnitField = ({ + name, + onChange, + singleSelection = false, +}: OrganisationUnitFieldProps) => { const { input, meta } = useField< - OrganisationUnitFormValue[], + OrganisationUnitFormValue[] | '', HTMLElement, OrganisationUnitFormValue[] - >(name ?? 'organisationUnits') + >(name ?? 'organisationUnits', { + format: (value) => (value === '' ? [] : value), + }) const roots = useCurrentUserRootOrgUnits() @@ -35,7 +43,9 @@ export const OrganisationUnitField = ({ name }: OrganisationUnitFieldProps) => { id, path, }) => { - const prevSelected = new Map(input.value.map((ou) => [ou.path, ou])) + const prevSelected = input.value + ? new Map(input.value.map((ou) => [ou.path, ou])) + : new Map() const newSelected = selected.map((selectedPath) => { const prev = prevSelected.get(selectedPath) return prev ?? { id, path, displayName } @@ -43,6 +53,9 @@ export const OrganisationUnitField = ({ name }: OrganisationUnitFieldProps) => { input.onChange(newSelected) input.onBlur() + if (onChange) { + onChange(newSelected) + } } const selectedPaths = input.value?.map((ou) => ou.path) ?? [] @@ -55,6 +68,7 @@ export const OrganisationUnitField = ({ name }: OrganisationUnitFieldProps) => { > ({ export const useOnSubmitNew = ({ section, + valueFormatter, }: { section: ModelSection + valueFormatter?: (values: TFormValues) => Record }) => { const createModel = useCreateModel(section.namePlural) const saveAlert = useAlert( @@ -80,7 +82,10 @@ export const useOnSubmitNew = ({ }) return } - const errors = await createModel(values) + const formattedValues = valueFormatter + ? valueFormatter(values) + : values + const errors = await createModel(formattedValues) if (errors) { return errors } diff --git a/src/lib/models/useCheckMaxLengthFromSchema.ts b/src/lib/models/useCheckMaxLengthFromSchema.ts index bc0a90d6..b5f4e19f 100644 --- a/src/lib/models/useCheckMaxLengthFromSchema.ts +++ b/src/lib/models/useCheckMaxLengthFromSchema.ts @@ -1,6 +1,5 @@ import { createMaxCharacterLength } from '@dhis2/ui' -import { useMemo } from 'react' -import { SchemaName } from '../../types' +import { SchemaFieldProperty, SchemaName } from '../../types' import { useSchema } from '../schemas' export function useCheckMaxLengthFromSchema( @@ -8,14 +7,14 @@ export function useCheckMaxLengthFromSchema( property: string ) { const schema = useSchema(model) - const maxLength = schema.properties[property].length - const checkMaxLength = useMemo( - () => - maxLength == undefined - ? () => undefined - : createMaxCharacterLength(maxLength), - [maxLength] - ) + return checkMaxLengthFromProperty(schema.properties[property]) +} - return checkMaxLength +export function checkMaxLengthFromProperty( + propertyDetails: SchemaFieldProperty +): (value: unknown) => string | undefined { + const maxLength = propertyDetails.length + return maxLength == undefined + ? () => undefined + : createMaxCharacterLength(maxLength) } diff --git a/src/lib/models/useFieldValidators.ts b/src/lib/models/useFieldValidators.ts new file mode 100644 index 00000000..3a9fc586 --- /dev/null +++ b/src/lib/models/useFieldValidators.ts @@ -0,0 +1,43 @@ +import { Validator } from '@dhis2/ui' +import { useMemo } from 'react' +import { useParams } from 'react-router-dom' +import { SchemaFieldPropertyType, SchemaSection } from '../../types' +import { composeAsyncValidators, required } from '../form' +import { useSchema } from '../schemas' +import { checkMaxLengthFromProperty } from './useCheckMaxLengthFromSchema' +import { useIsFieldValueUnique } from './useIsFieldValueUnique' + +export function useValidator({ + schemaSection, + property, +}: { + schemaSection: SchemaSection + property: string +}) { + const schema = useSchema(schemaSection.name) + const propertyDetails = schema.properties[property] + + const validators = useMemo(() => [] as Validator[], []) + const params = useParams() + const modelId = params.id as string + const checkMaxLength = checkMaxLengthFromProperty(propertyDetails) + const checkIsValueTaken = useIsFieldValueUnique({ + model: schemaSection.namePlural, + field: property, + id: modelId, + }) as Validator + if (propertyDetails.propertyType === SchemaFieldPropertyType.REFERENCE) { + validators.push(checkMaxLength) + } + if (propertyDetails.unique) { + validators.push(checkIsValueTaken) + } + if (propertyDetails.required) { + validators.push(required) + } + + return useMemo( + () => composeAsyncValidators(validators), + [validators] + ) +} diff --git a/src/lib/zod/getDefaults.ts b/src/lib/zod/getDefaults.ts index 3e26dc5d..e7c638e9 100644 --- a/src/lib/zod/getDefaults.ts +++ b/src/lib/zod/getDefaults.ts @@ -12,7 +12,6 @@ type WrapNonDefaultsInOptional = T extends z.ZodEffects< // inspired by: https://github.com/colinhacks/zod/discussions/1953#discussioncomment-5695528 // added some type-improvements -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function getDefaults>( schema: T ): z.infer> { @@ -34,15 +33,13 @@ export function getDefaults>( if (innerSchema instanceof z.ZodArray) { return [] } - // return an empty string if it is - if (innerSchema instanceof z.ZodString) { - return '' - } // return an content of object recursivly if (innerSchema instanceof z.ZodObject) { return getDefaults(innerSchema) } - + if (innerSchema instanceof z.ZodOptional) { + return undefined + } if (!('innerType' in innerSchema._def)) { return undefined } diff --git a/src/pages/dataElements/form/dataElementSchema.ts b/src/pages/dataElements/form/dataElementSchema.ts index ce60b84d..e4ee265b 100644 --- a/src/pages/dataElements/form/dataElementSchema.ts +++ b/src/pages/dataElements/form/dataElementSchema.ts @@ -1,5 +1,4 @@ import { z } from 'zod' -import { DataElement } from '../../../types/generated' export const dataElementSchema = z .object({ diff --git a/src/pages/organisationUnits/New.tsx b/src/pages/organisationUnits/New.tsx new file mode 100644 index 00000000..d50a541a --- /dev/null +++ b/src/pages/organisationUnits/New.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { FormBase } from '../../components' +import { DefaultNewFormContents } from '../../components/form/DefaultFormContents' +import { SECTIONS_MAP, useOnSubmitNew, validate } from '../../lib' +import { + FormValues, + initialValues, + OrganisationUnitFormField, + 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 = () => { + return ( + { + return validate(organisationUnitSchema, values) + }} + > + + + + + ) +} diff --git a/src/pages/organisationUnits/form/ImageField.module.css b/src/pages/organisationUnits/form/ImageField.module.css new file mode 100644 index 00000000..c1be1b9f --- /dev/null +++ b/src/pages/organisationUnits/form/ImageField.module.css @@ -0,0 +1,12 @@ +.fileInputWrapper { + display: flex; + flex-wrap: wrap; + flex-direction: column; + max-width: 400px; + gap: var(--spacers-dp4); +} + +.fileInputWrapper img { + height: 36px; + max-width: 200px; +} diff --git a/src/pages/organisationUnits/form/ImageField.tsx b/src/pages/organisationUnits/form/ImageField.tsx new file mode 100644 index 00000000..e130c283 --- /dev/null +++ b/src/pages/organisationUnits/form/ImageField.tsx @@ -0,0 +1,145 @@ +import { useConfig, useDataEngine } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + FileInput, + FileInputChangeHandler, + FileList, + FileListItem, + Field as UIField, + Help, +} from '@dhis2/ui' +import React, { useState } from 'react' +import { useField } from 'react-final-form' +import css from './ImageField.module.css' + +const fileToBase64 = (file: File): Promise => { + const reader = new FileReader() + + return new Promise((resolve, reject) => { + reader.readAsDataURL(file) + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + }) +} + +export function ImageField() { + const dataEngine = useDataEngine() + const fieldName = 'image' + const { input } = useField(fieldName, { format: (value) => value }) + + const [fileBase64, setFileBase64] = useState() + + const uploadFile = async (fileToUpload: File) => { + const fileToUploadDetails = { + name: fileToUpload.name, + size: fileToUpload.size, + } + updateInputValue({ + ...fileToUploadDetails, + id: undefined, + error: undefined, + }) + try { + const postResponse = (await dataEngine.mutate({ + resource: 'fileResources', + type: 'create', + data: { file: fileToUpload }, + })) as { + response: { + fileResource: { id: string; storageStatus: string } + } + } + updateInputValue({ + ...fileToUploadDetails, + id: postResponse.response.fileResource.id, + error: undefined, + }) + } catch (e) { + console.error(e) + updateInputValue({ + ...fileToUploadDetails, + id: undefined, + error: (e as Error | string).toString(), + }) + } + } + const updateInputValue = (newInputValue: { + name: string + size: number + id?: string + error?: string + }) => { + input.onChange(newInputValue) + input.onBlur() + } + const deleteFile = () => { + setFileBase64(undefined) + input.onChange(undefined) + input.onBlur() + } + const handleChange: FileInputChangeHandler = async ({ files }) => { + const newFile = files[0] + if (newFile instanceof File) { + uploadFile(newFile) + const file64 = await fileToBase64(newFile) + setFileBase64(file64) + } + } + + return ( + +
+ + +
+ + + {input.value?.id && ( + + )} + + + {i18n.t( + 'Max size 5MB. Supported file size are .jpg, .png, and .gif.' + )} + +
+ ) +} + +const ImagePreview = ({ + fileBase64, + fileResource, +}: { + fileBase64?: string + fileResource?: { id: string } +}) => { + const baseUrl = useConfig().baseUrl + + if (fileBase64) { + return {i18n.t('Preview + } + + if (fileResource && fileResource.id) { + const src = `${baseUrl}/fileResources/${fileResource.id}/data` + + return {i18n.t('Preview + } + return null +} diff --git a/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx b/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx new file mode 100644 index 00000000..9bb44bb9 --- /dev/null +++ b/src/pages/organisationUnits/form/OrganisationUnitFormFields.tsx @@ -0,0 +1,149 @@ +import i18n from '@dhis2/d2-i18n' +import { InputFieldFF } from '@dhis2/ui' +import React from 'react' +import { Field as FieldRFF } from 'react-final-form' +import { + CustomAttributesSection, + StandardFormField, + StandardFormSection, + StandardFormSectionDescription, + StandardFormSectionTitle, +} from '../../../components' +import { + DefaultIdentifiableFields, + DescriptionField, +} from '../../../components/form' +import { DateField } from '../../../components/form/fields/DateField' +import { SCHEMA_SECTIONS } from '../../../lib' +import { ImageField } from './ImageField' +import { OrganisationUnitSelector } from './OrganisationUnitSelector' + +const schemaSection = SCHEMA_SECTIONS.organisationUnit + +export function OrganisationUnitFormField() { + return ( + <> + + + {i18n.t('Placement in hierarchy')} + + + {i18n.t( + 'Choose where this new organisation unit should be placed in the existing hierarchy' + )} + + + + + + + + {i18n.t('Basic information')} + + + + {i18n.t( + 'Set up the basic information for this organisation unit.' + )} + + + + + + + + + + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('Comment')} + name="comment" + /> + + + + + + + + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('Contact person')} + name="contactPerson" + /> + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('Address')} + name="address" + /> + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('Email')} + name="email" + type="email" + /> + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('Phone number')} + name="phoneNumber" + type="tel" + /> + + + component={InputFieldFF} + inputWidth="400px" + label={i18n.t('URL')} + name="url" + type="url" + helpText={i18n.t( + 'A web link that provides extra information.' + )} + /> + + + + + {i18n.t('Location')} + + + + {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" + /> + + + + + ) +} diff --git a/src/pages/organisationUnits/form/OrganisationUnitSelector.module.css b/src/pages/organisationUnits/form/OrganisationUnitSelector.module.css new file mode 100644 index 00000000..a5a575cc --- /dev/null +++ b/src/pages/organisationUnits/form/OrganisationUnitSelector.module.css @@ -0,0 +1,17 @@ +.selectedOrgUnitInfo { + display: flex; + gap: 4px; + flex-direction: row; + align-items: center; +} + +.selectedOrgUnitBox { + width: 600px; + height: 350px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + resize: both; + overflow: auto; + white-space: pre-wrap; +} diff --git a/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx b/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx new file mode 100644 index 00000000..16ba244a --- /dev/null +++ b/src/pages/organisationUnits/form/OrganisationUnitSelector.tsx @@ -0,0 +1,67 @@ +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 { useField } from 'react-final-form' +import { useCurrentUserRootOrgUnits } from '../../../lib/user/currentUserStore' +import classes from './OrganisationUnitSelector.module.css' + +export function OrganisationUnitSelector() { + const fieldName = 'parent' + const { input, meta } = useField(fieldName, { format: (value) => value }) + const userRootOrgUnits = useCurrentUserRootOrgUnits() + const userRootOrgUnitsIds = userRootOrgUnits.map((unit) => `/${unit.id}`) + const [selected, setSelected] = useState<[string] | []>([]) + + const handleChange = (orgUnit: { + displayName: string + id: string + path: string + }) => { + input.onChange({ + displayName: orgUnit.displayName, + id: orgUnit.id, + path: orgUnit.path, + }) + setSelected([orgUnit.path]) + input.onBlur() + } + + return ( + + {userRootOrgUnits.length > 0 ? ( + <> +
+ +
+ {input.value?.displayName && ( +
+ +

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

+
+ )} + + ) : ( + + {i18n.t( + 'This is the first organisation unit and will be created as the root of the hierarchy.' + )} + + )} +
+ ) +} diff --git a/src/pages/organisationUnits/form/index.ts b/src/pages/organisationUnits/form/index.ts new file mode 100644 index 00000000..c5bde466 --- /dev/null +++ b/src/pages/organisationUnits/form/index.ts @@ -0,0 +1,5 @@ +export { OrganisationUnitFormField } from './OrganisationUnitFormFields' +export { ImageField } from './ImageField' +export { OrganisationUnitSelector } from './OrganisationUnitSelector' +export { organisationUnitSchema, initialValues } from './organisationUnitSchema' +export type { FormValues } from './types' diff --git a/src/pages/organisationUnits/form/organisationUnitSchema.ts b/src/pages/organisationUnits/form/organisationUnitSchema.ts new file mode 100644 index 00000000..262e5df7 --- /dev/null +++ b/src/pages/organisationUnits/form/organisationUnitSchema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod' +import { getDefaults, modelFormSchemas } from '../../../lib' + +const { withAttributeValues, identifiable } = modelFormSchemas + +export const organisationUnitSchema = identifiable + .merge(withAttributeValues) + .extend({ + shortName: z.string().trim().default(''), + code: z.string().trim().optional(), + description: z.string().trim().optional(), + image: z.object({ id: z.string() }).optional(), + phoneNumber: z.string().optional(), + contactPerson: z.string().optional(), + openingDate: z.string(), + email: z.string().optional(), + address: z.string().optional(), + url: z.string().optional(), + closedDate: z.string().optional(), + parent: z.object({ id: z.string() }).optional(), + geometry: z + .object({ + longitude: z.string().optional(), + latitude: z.string().optional(), + }) + .optional(), + }) + +export const initialValues = getDefaults( + organisationUnitSchema as z.AnyZodObject +) diff --git a/src/pages/organisationUnits/form/types.ts b/src/pages/organisationUnits/form/types.ts new file mode 100644 index 00000000..5a5be5cf --- /dev/null +++ b/src/pages/organisationUnits/form/types.ts @@ -0,0 +1,3 @@ +import { OrganisationUnit } from '../../../types/generated' + +export type FormValues = OrganisationUnit