diff --git a/i18n/en.pot b/i18n/en.pot index 07aee14b3..327c69ed9 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-11-04T20:16:04.917Z\n" -"PO-Revision-Date: 2024-11-04T20:16:04.918Z\n" +"POT-Creation-Date: 2024-11-06T18:10:35.150Z\n" +"PO-Revision-Date: 2024-11-06T18:10:35.150Z\n" msgid "schemas" msgstr "schemas" @@ -411,6 +411,15 @@ msgstr "Translation updated successfully" msgid "Save translations" msgstr "Save translations" +msgid "Go back" +msgstr "Go back" + +msgid "Next section" +msgstr "Next section" + +msgid "Save and exit" +msgstr "Save and exit" + msgid "Can edit and capture" msgstr "Can edit and capture" @@ -1091,15 +1100,36 @@ msgstr "" "included. PHU will still be available for the PHU level, but not included " "in the aggregations to the levels above." +msgid "Set up the basic information for this data set." +msgstr "Set up the basic information for this data set." + +msgid "Configure data elements" +msgstr "Configure data elements" + +msgid "Choose what data is collected for this data set." +msgstr "Choose what data is collected for this data set." + +msgid "Configure data entry periods" +msgstr "Configure data entry periods" + +msgid "Choose for what time periods data can be entered for this data set" +msgstr "Choose for what time periods data can be entered for this data set" + msgid "Setup" msgstr "Setup" msgid "Data" msgstr "Data" +msgid "Data Elements" +msgstr "Data Elements" + msgid "Periods" msgstr "Periods" +msgid "Period type" +msgstr "Period type" + msgid "Organisation Units" msgstr "Organisation Units" diff --git a/src/lib/form/index.ts b/src/lib/form/index.ts index 2f8ef9f18..0a796e6c5 100644 --- a/src/lib/form/index.ts +++ b/src/lib/form/index.ts @@ -6,4 +6,3 @@ export { validate, createFormValidate } from './validate' export { useOnSubmitEdit, useOnSubmitNew } from './useOnSubmit' export { modelFormSchemas } from './modelFormSchemas' export * from './sectionedForm' -export * from './sectionedForm/sectionedFormDescriptor' diff --git a/src/lib/form/sectionedForm/SectionedFormBase.tsx b/src/lib/form/sectionedForm/SectionedFormBase.tsx deleted file mode 100644 index fcfc0c016..000000000 --- a/src/lib/form/sectionedForm/SectionedFormBase.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import { SectionedFormProvider } from './SectionedFormContext' - -type SectionedFormProps = { - name: string - children: React.ReactNode -} -export const SectionedFormBase = ({ name, children }: SectionedFormProps) => { - return ( - - {children} - - ) -} diff --git a/src/lib/form/sectionedForm/SectionedFormContext.tsx b/src/lib/form/sectionedForm/SectionedFormContext.tsx index a84e87b8d..e78764380 100644 --- a/src/lib/form/sectionedForm/SectionedFormContext.tsx +++ b/src/lib/form/sectionedForm/SectionedFormContext.tsx @@ -1,20 +1,89 @@ import React, { createContext, useState } from 'react' -import { createFormStore, FormProps, FormStore } from './formStore' +import { SectionDescriptor, SectionedFormDescriptor } from './formDescriptor' -export const SectionedFormContext = createContext(null) +/* Some of the types in this file may look complex. + However they are here to help type-safety and autocommpletion for consumers. + + The only thing consumers need to do is pass the type of the formdescriptor to use the context. + useSectionedFormDescriptor() + This helps usage in specific form components. +*/ -export const SectionedFormProvider = ({ +type AllFieldNames = + T['sections'][number]['fields'][number]['name'] + +/* Helper to avoid returning undefined from a map when we know we have the value from the type. +And conversely - add undefined to TType if we dont have a specifc type for T*/ +type EnforceIfInferrable< + T extends SectionedFormDescriptor, + TType +> = T extends SectionedFormDescriptor + ? unknown extends U + ? TType | undefined + : TType + : never + +function createContextValue( + descriptor: T +) { + const fieldLabels = Object.fromEntries( + descriptor.sections.flatMap((section) => + section.fields.map((f) => [f.name, f.label] as const) + ) + ) as Record, EnforceIfInferrable> + + const sectionMap = Object.fromEntries( + descriptor.sections.map((s) => [s.name, s]) + ) as Record< + T['sections'][number]['name'], + EnforceIfInferrable + > + + const sections: T['sections'] = descriptor.sections + return { + formName: descriptor.name, + formLabel: descriptor.label, + sections, + getSection: (name: T['sections'][number]['name']) => sectionMap[name], + getFieldLabel: (field: AllFieldNames) => { + return fieldLabels[field] + }, + } +} +type SectionFormContextValue = ReturnType< + typeof createContextValue +> + +export const SectionedFormContext = createContext | null>(null) + +export const SectionedFormDescriptorProvider = < + T extends SectionedFormDescriptor +>({ children, initialValue, }: { - initialValue: Partial + initialValue: T children: React.ReactNode }) => { - const [store] = useState(() => createFormStore(initialValue)) + const [contextValue] = useState(() => createContextValue(initialValue)) return ( - + {children} ) } + +export const useSectionedFormDescriptor = < + T extends SectionedFormDescriptor +>() => { + const context = React.useContext(SectionedFormContext) + if (!context) { + throw new Error( + 'useSectionedFormDescriptor must be used within a SectionedFormDescriptorProvider' + ) + } + return context as SectionFormContextValue +} diff --git a/src/lib/form/sectionedForm/SectionedFormDescriptorProvider.tsx b/src/lib/form/sectionedForm/SectionedFormDescriptorProvider.tsx deleted file mode 100644 index b785cdf71..000000000 --- a/src/lib/form/sectionedForm/SectionedFormDescriptorProvider.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { createContext, useState } from 'react' -import { - SectionDescriptor, - SectionedFormDescriptor, -} from './sectionedFormDescriptor' - -/* Some of the types in this file may look complex. - However they are here to help type-safety and autocommpletion for consumers. - - The only thing consumers need to do is pass the type of the formdescriptor to use the context. - useSectionedFormDescriptor() - This helps usage in specific form components. -*/ - -type AllFieldNames = - T['sections'][number]['fields'][number]['name'] - -/* Helper to avoid returning undefined from a map when we know we have the value from the type. -And conversely - add undefined to TType if we dont have a specifc type for T*/ -type EnforceIfInferrable< - T extends SectionedFormDescriptor, - TType -> = T extends SectionedFormDescriptor - ? unknown extends U - ? TType | undefined - : TType - : never - -function createContextValue(descriptor: T) { - const fieldLabels = Object.fromEntries( - descriptor.sections.flatMap((section) => - section.fields.map((f) => [f.name, f.label] as const) - ) - ) as Record, EnforceIfInferrable> - - const sectionMap = Object.fromEntries( - descriptor.sections.map((s) => [s.name, s]) - ) as Record< - T['sections'][number]['name'], - EnforceIfInferrable - > - - const sections: T['sections'] = descriptor.sections - return { - formName: descriptor.name, - formLabel: descriptor.label, - sections, - getSection: (name: T['sections'][number]['name']) => sectionMap[name], - getFieldLabel: (field: AllFieldNames) => { - return fieldLabels[field] - }, - } -} -type SectionFormContextValue = ReturnType< - typeof createContextValue -> - -export const SectionedFormContext = createContext | null>(null) - -export const SectionedFormDescriptorProvider = < - T extends SectionedFormDescriptor ->({ - children, - initialValue, -}: { - initialValue: T - children: React.ReactNode -}) => { - const [contextValue] = useState(() => createContextValue(initialValue)) - - return ( - - {children} - - ) -} - -export const useSectionedFormDescriptor = < - T extends SectionedFormDescriptor ->() => { - const context = React.useContext(SectionedFormContext) - if (!context) { - throw new Error( - 'useSectionedFormDescriptor must be used within a SectionedFormDescriptorProvider' - ) - } - return context as SectionFormContextValue -} diff --git a/src/lib/form/sectionedForm/SectionedFormField.tsx b/src/lib/form/sectionedForm/SectionedFormField.tsx deleted file mode 100644 index 1bf058cb1..000000000 --- a/src/lib/form/sectionedForm/SectionedFormField.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { useEffect } from 'react' -import { useRegisterField } from './useRegister' - -type SectionedFormField = { - label: string - name: string - children: React.ReactNode -} -export const SectionFormField = ({ - label, - name, - children, -}: SectionedFormField) => { - const register = useRegisterField() - - useEffect(() => { - register({ name, label }) - }, [register, name, label]) - - return children -} diff --git a/src/lib/form/sectionedForm/SectionedFormSection.tsx b/src/lib/form/sectionedForm/SectionedFormSection.tsx deleted file mode 100644 index 79af2c3db..000000000 --- a/src/lib/form/sectionedForm/SectionedFormSection.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { SectionedFormSectionProvider } from './SectionedFormSectionContext' -import { createSectionStore } from './sectionStore' -import { SectionedFormSectionContext } from './SectionedFormSectionContext' -import { useRegisterFormSection } from './useRegister' - -type SectionFormSectionProps = { - label: string - name: string - children: React.ReactNode -} -export const SectionedFormSection = ({ - label, - name, - children, -}: SectionFormSectionProps) => { - return ( - - {children} - - ) -} diff --git a/src/lib/form/sectionedForm/SectionedFormSectionContext.tsx b/src/lib/form/sectionedForm/SectionedFormSectionContext.tsx deleted file mode 100644 index 7ee63ec10..000000000 --- a/src/lib/form/sectionedForm/SectionedFormSectionContext.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { createContext, useEffect, useState } from 'react' -import { useStore } from 'zustand' -import { SectionedFormContext } from './SectionedFormContext' -import { createSectionStore, SectionProps, SectionStore } from './sectionStore' - -export const SectionedFormSectionContext = createContext( - null -) - -export const SectionedFormSectionProvider = ({ - children, - initialValue, -}: { - initialValue: SectionProps - children: React.ReactNode -}) => { - const [store] = useState(() => createSectionStore(initialValue)) - const formContext = React.useContext(SectionedFormContext) - if (!formContext) { - throw new Error( - 'SectionedFormSectionProvider must be wrapped in a SectionFormSectionProvider' - ) - } - - const addSectionToForm = useStore(formContext, (state) => state.addSection) - - useEffect(() => { - addSectionToForm(store.getState().section) - }, [store, addSectionToForm]) - - return ( - - {children} - - ) -} diff --git a/src/lib/form/sectionedForm/formStore.ts b/src/lib/form/sectionedForm/formStore.ts deleted file mode 100644 index 6bc7db713..000000000 --- a/src/lib/form/sectionedForm/formStore.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createStore } from 'zustand' -import { devtools } from 'zustand/middleware' -import { uniqueBy } from '../../../lib/utils' -import { SectionedFormSection, SectionFormField } from './types' - -type SectionIdentifier = string | SectionedFormSection - -export interface FormProps { - name: string - sections: SectionedFormSection[] - sectionFields: Map -} - -export interface FormState extends FormProps { - addSection: (section: SectionedFormSection) => void - addField: ( - section: string | SectionedFormSection, - field: SectionFormField - ) => void - getFieldsForSection: (section: SectionIdentifier) => SectionFormField[] - getSectionsForField: ( - field: string | SectionFormField - ) => SectionedFormSection[] | undefined -} - -export type FormStore = ReturnType - -export const createFormStore = (initialProps: Partial) => - createStore()( - devtools((set, get) => ({ - name: '', - sections: [], - sectionFields: new Map(), - ...initialProps, - addSection: (section: SectionedFormSection) => { - const prevSections = get().sections - set({ sections: prevSections.concat(section) }) - }, - addField: (section, field) => { - const sectionName = resolveSectionName(section) - const prevFieldsMap = get().sectionFields - const newFields = uniqueBy( - prevFieldsMap.get(sectionName)?.concat(field) || [field], - (field) => field.name - ) - - const sectionFields = new Map(prevFieldsMap).set( - sectionName, - newFields - ) - set({ sectionFields }) - }, - getSections: () => get().sections, - getFieldsForSection: (section: SectionIdentifier) => { - const sectionName = resolveSectionName(section) - return get().sectionFields.get(sectionName) || [] - }, - getSectionsForField: (field: string | SectionFormField) => { - const fieldName = typeof field === 'string' ? field : field.name - const fieldsBySection = get().sectionFields - for (const [section, fields] of fieldsBySection.entries()) { - if (fields.find((f) => f.name === fieldName)) { - return get().sections.filter((s) => s.name === section) - } - } - return undefined - }, - })) - ) - -const resolveSectionName = (section: SectionIdentifier) => { - return typeof section === 'string' ? section : section.name -} diff --git a/src/lib/form/sectionedForm/index.ts b/src/lib/form/sectionedForm/index.ts index d22f6e6db..c851e06ed 100644 --- a/src/lib/form/sectionedForm/index.ts +++ b/src/lib/form/sectionedForm/index.ts @@ -1,6 +1,6 @@ -export { SectionFormField } from './SectionedFormField' -export { SectionedFormSection } from './SectionedFormSection' -export { SectionedFormBase } from './SectionedFormBase' -export { useSectionedFormState } from './useSectionedFormState' export * from './useSelectedSection' -export * from './SectionedFormDescriptorProvider' +export { + useSectionedFormDescriptor, + SectionedFormDescriptorProvider, +} from './SectionedFormContext' +export * from './types' diff --git a/src/lib/form/sectionedForm/sectionStore.ts b/src/lib/form/sectionedForm/sectionStore.ts deleted file mode 100644 index fbea0cd35..000000000 --- a/src/lib/form/sectionedForm/sectionStore.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { create, createStore } from 'zustand' -import { devtools } from 'zustand/middleware' -import { SectionedFormSection, SectionFormField } from './types' - -export interface SectionProps { - section: SectionedFormSection -} - -export interface SectionState extends SectionProps { - setSection: (section: SectionedFormSection) => void - getSection: () => SectionedFormSection -} -export type SectionStore = ReturnType - -export const createSectionStore = (initialProps: SectionProps) => - createStore()( - devtools((set, get) => ({ - ...initialProps, - setSection: (section: SectionedFormSection) => { - set({ section }) - }, - getSection: () => { - return get().section - }, - })) - ) diff --git a/src/lib/form/sectionedForm/types.ts b/src/lib/form/sectionedForm/types.ts index 5dd3a6ca7..d57a62641 100644 --- a/src/lib/form/sectionedForm/types.ts +++ b/src/lib/form/sectionedForm/types.ts @@ -1,9 +1,20 @@ -export type SectionedFormSection = { - name: string +export type FieldDescriptor = { label: string + // keyof T | (string & {}) allows auto-completion for keys of T, while also allowing + // any other string to be used as a key. This allows fields that not necessarily map to the model-property + name: keyof T | (string & {}) +} + +export type SectionDescriptor = { + label: string + name: string + // keyof T | (string & {}) allows auto-completion for fields, while also allowing + // any other string to be used as a key + fields: FieldDescriptor[] } -export type SectionFormField = { +export type SectionedFormDescriptor = { name: string label: string + sections: SectionDescriptor[] } diff --git a/src/lib/form/sectionedForm/useRegister.ts b/src/lib/form/sectionedForm/useRegister.ts deleted file mode 100644 index 31aa7efee..000000000 --- a/src/lib/form/sectionedForm/useRegister.ts +++ /dev/null @@ -1,28 +0,0 @@ -import React, { useCallback } from 'react' -import { useStore } from 'zustand' -import { SectionedFormContext } from './SectionedFormContext' -import { SectionedFormSectionContext } from './SectionedFormSectionContext' -import { SectionFormField } from './types' - -export const useRegisterField = () => { - const sectionContext = React.useContext(SectionedFormSectionContext)! - const formContext = React.useContext(SectionedFormContext)! - - const currentSection = useStore(sectionContext, (state) => - state.getSection() - ) - - const addFieldToForm = useStore(formContext, (state) => state.addField) - - return useCallback( - (field: SectionFormField) => { - if (currentSection) { - addFieldToForm(currentSection, field) - } else { - console.error(`Tried to register field ${field.name} in section, but no section is set. -Make sure to wrap fields in a SectionedFormSection component`) - } - }, - [addFieldToForm, currentSection] - ) -} diff --git a/src/lib/form/sectionedForm/useSectionedFormState.ts b/src/lib/form/sectionedForm/useSectionedFormState.ts deleted file mode 100644 index 0987fa3ef..000000000 --- a/src/lib/form/sectionedForm/useSectionedFormState.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useContext } from 'react' -import { useStore } from 'zustand' -import { FormState } from './formStore' -import { SectionedFormContext } from './SectionedFormContext' - -export function useSectionedFormState(): FormState -export function useSectionedFormState(selector: (state: FormState) => T): T -export function useSectionedFormState(selector?: (state: FormState) => T) { - const formStore = useContext(SectionedFormContext) - if (!formStore) { - throw new Error( - 'useFormState must be used within a SectionedFormProvider' - ) - } - return useStore(formStore, selector!) -} diff --git a/src/lib/form/sectionedForm/useSelectedSection.ts b/src/lib/form/sectionedForm/useSelectedSection.ts index 96fe38660..75d2fbcd9 100644 --- a/src/lib/form/sectionedForm/useSelectedSection.ts +++ b/src/lib/form/sectionedForm/useSelectedSection.ts @@ -1,11 +1,6 @@ import { useMemo } from 'react' -import { - useQueryParam, - StringParam, - createEnumParam, - withDefault, -} from 'use-query-params' -import { useSectionedFormDescriptor } from './SectionedFormDescriptorProvider' +import { useQueryParam, createEnumParam, withDefault } from 'use-query-params' +import { useSectionedFormDescriptor } from './SectionedFormContext' export const FORM_SECTION_PARAM_KEY = 'section' @@ -25,7 +20,6 @@ export const useSelectedSection = () => { [sections] ) - console.log({ paramConfig }) return useQueryParam(FORM_SECTION_PARAM_KEY, paramConfig, { removeDefaultsFromUrl: true, })