Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCRVS 6085: Configurable handlebar helpers #6086

Merged
merged 8 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/client/src/components/form/FormFieldGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ class FormSectionComponent extends React.Component<Props> {
offlineCountryConfig,
intl,
draftData,
userDetails,
setValues,
dynamicDispatch
} = this.props
Expand All @@ -947,7 +948,8 @@ class FormSectionComponent extends React.Component<Props> {
field,
{ ...draftData?.[sectionName], ...values },
offlineCountryConfig,
draftData
draftData,
userDetails
)

if (conditionalActions.includes('hide')) {
Expand Down
30 changes: 30 additions & 0 deletions packages/client/src/forms/handlebarHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

import { referenceApi } from '@client/utils/referenceApi'
import * as Handlebars from 'handlebars'

export let handlebarHelpers: Record<string, Handlebars.HelperDelegate>

export async function initHandlebarHelpers() {
handlebarHelpers = await referenceApi.importHandlebarHelpers()
}

export function registerHandlebarHelpers() {
if (handlebarHelpers) {
for (const funcName of Object.keys(handlebarHelpers)) {
const func = handlebarHelpers[funcName]
if (typeof func === 'function') {
Handlebars.registerHelper(funcName, func)
}
}
}
}
3 changes: 2 additions & 1 deletion packages/client/src/forms/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,8 @@ export const getConditionalActionsForField = (
*/
values: IFormSectionData,
offlineCountryConfig?: IOfflineData,
draftData?: IFormData
draftData?: IFormData,
userDetails?: UserDetails | null
): string[] => {
if (!field.conditionals) {
return []
Expand Down
15 changes: 14 additions & 1 deletion packages/client/src/offline/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
IApplicationConfigAnonymous,
LoadFormsResponse,
LoadValidatorsResponse,
LoadConditionalsResponse
LoadConditionalsResponse,
LoadHandlebarHelpersResponse
} from '@client/utils/referenceApi'
import { System } from '@client/utils/gateway'
import { UserDetails } from '@client/utils/userUtils'
Expand Down Expand Up @@ -324,6 +325,16 @@ export const validatorsFailed = (error: Error) => ({
payload: error
})

export const handlebarsLoaded = (payload: LoadHandlebarHelpersResponse) => ({
type: 'OFFLINE/HANDLEBARS_LOADED' as const,
payload: payload
})

export const handlebarsFailed = (error: Error) => ({
type: 'OFFLINE/HANDLEBARS_FAILED' as const,
payload: error
})

export const conditionalsLoaded = (payload: LoadConditionalsResponse) => ({
type: 'OFFLINE/CONDITIONALS_LOADED' as const,
payload: payload
Expand Down Expand Up @@ -367,3 +378,5 @@ export type Action =
| ReturnType<typeof validatorsFailed>
| ReturnType<typeof conditionalsLoaded>
| ReturnType<typeof conditionalsFailed>
| ReturnType<typeof handlebarsLoaded>
| ReturnType<typeof handlebarsFailed>
7 changes: 7 additions & 0 deletions packages/client/src/offline/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
Action as NotificationAction,
configurationErrorNotification
} from '@client/notification/actions'
import { initHandlebarHelpers } from '@client/forms/handlebarHelpers'

export const OFFLINE_LOCATIONS_KEY = 'locations'
export const OFFLINE_FACILITIES_KEY = 'facilities'
Expand Down Expand Up @@ -236,6 +237,11 @@ const VALIDATORS_CMD = Cmd.run(() => initValidators(), {
failActionCreator: actions.validatorsFailed
})

const HANDLEBARS_CMD = Cmd.run(() => initHandlebarHelpers(), {
successActionCreator: actions.handlebarsLoaded,
failActionCreator: actions.handlebarsFailed
})

const RETRY_TIMEOUT = 5000

function delay(cmd: RunCmd<any>, time: number) {
Expand All @@ -252,6 +258,7 @@ function getDataLoadingCommands() {
CONFIG_CMD,
CONDITIONALS_CMD,
VALIDATORS_CMD,
HANDLEBARS_CMD,
FORMS_CMD,
CONTENT_CMD
])
Expand Down
14 changes: 13 additions & 1 deletion packages/client/src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ vi.mock('@client/forms/validators', async () => {
}
})

vi.mock('@client/forms/handlebarHelpers', async () => {
const actual = (await vi.importActual(
'@client/forms/handlebarHelpers'
)) as any
return {
...actual,
handlebarHelpers: {},
initHandlebarHelpers: () => Promise.resolve()
}
})

/*
* Initialize mocks
*/
Expand Down Expand Up @@ -181,7 +192,8 @@ vi.doMock(
loadConfigAnonymousUser: () => Promise.resolve(mockConfigResponse),
loadForms: () => Promise.resolve(mockOfflineData.forms.forms),
importConditionals: () => Promise.resolve({}),
importValidators: () => Promise.resolve({})
importValidators: () => Promise.resolve({}),
importHandlebarHelpers: () => Promise.resolve({})
}
})
)
Expand Down
14 changes: 14 additions & 0 deletions packages/client/src/utils/referenceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@ export async function importConditionals(): Promise<LoadConditionalsResponse> {
return conditionals
}

export type LoadHandlebarHelpersResponse = Record<
string,
Handlebars.HelperDelegate
>
async function importHandlebarHelpers(): Promise<LoadHandlebarHelpersResponse> {
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
const handlebars = await import(
/* @vite-ignore */ `${window.config.COUNTRY_CONFIG_URL}/handlebars.js`
)

return handlebars
}

async function loadContent(): Promise<IContentResponse> {
const url = `${window.config.COUNTRY_CONFIG_URL}/content/client`

Expand Down Expand Up @@ -345,5 +358,6 @@ export const referenceApi = {
loadForms,
importValidators,
importConditionals,
importHandlebarHelpers,
loadConfigAnonymousUser
}
2 changes: 2 additions & 0 deletions packages/client/src/views/PrintCertificate/PDFUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { fetchImageAsBase64 } from '@client/utils/imageUtils'
import { getOfflineData } from '@client/offline/selectors'
import isValid from 'date-fns/isValid'
import format from 'date-fns/format'
import { registerHandlebarHelpers } from '@client/forms/handlebarHelpers'

type TemplateDataType = string | MessageDescriptor | Array<string>
function isMessageDescriptor(
Expand Down Expand Up @@ -97,6 +98,7 @@ export function executeHandlebarsTemplate(
},
cache
)
registerHandlebarHelpers()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pass intl down to the helpers from here so that custom handlebar helpers can localise the values in use?

export function registerHandlebarHelpers(intl: IntlShape) {
  for (const funcName of Object.keys(handlebarHelpers)) {
    const func = handlebarHelpers[funcName]({ intl })
    if (typeof func === 'function') {
      Handlebars.registerHelper(funcName, func)
    }
  }
}

So the definition for a function handlebar helper in country config would become

function myHelper(options: { intl: IntlShape }) {
  return (valueToTransform: string) => valueToTransform
}

or just

function myHelper() {
  return (valueToTransform: string) => valueToTransform
}

if intl is not needed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to, but going to come back to that in a different PR. At the moment I am struggling with extra params conflicting with the expected params to Handlebars.HelperDelegate


Handlebars.registerHelper(
'intl',
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/views/RegisterForm/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,8 @@ class RegisterFormView extends React.Component<FullProps, State> {
informant = 'mother'
} else if (declaration?.data?.informant?.informantType === 'FATHER') {
informant = 'father'
} else if (declaration?.data?.informant?.informantType === 'SPOUSE') {
informant = 'spouse'
}

modifiedDeclaration = {
Expand Down
14 changes: 9 additions & 5 deletions packages/client/src/views/RegisterForm/review/ReviewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
}

isVisibleField(field: IFormField, section: IFormSection) {
const { draft, offlineCountryConfiguration } = this.props
const { draft, offlineCountryConfiguration, userDetails } = this.props

if (field.type === HIDDEN) {
return false
Expand All @@ -867,7 +867,8 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
field,
draft.data[section.id] || {},
offlineCountryConfiguration,
draft.data
draft.data,
userDetails
)
return (
!conditionalActions.includes('hide') &&
Expand Down Expand Up @@ -1358,6 +1359,7 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
getOverriddenFieldsListForPreview(
formSections: IFormSection[]
): IFormField[] {
const { userDetails } = this.props
const overriddenFields = formSections
.map((section) => {
return section.groups
Expand All @@ -1377,7 +1379,8 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
tempField,
draft.data[residingSection] || {},
offlineCountryConfiguration,
draft.data
draft.data,
userDetails
).includes('hide')

return isVisible ? field : ({} as IFormField)
Expand Down Expand Up @@ -1528,7 +1531,7 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
offlineCountryConfiguration: IOfflineData,
declaration: IDeclaration
) => {
const { intl, draft } = this.props
const { intl, draft, userDetails } = this.props
const overriddenFields =
this.getOverriddenFieldsListForPreview(formSections)
let tempItem: any
Expand All @@ -1553,7 +1556,8 @@ class ReviewSectionComp extends React.Component<FullProps, State> {
field,
draft.data[section.id] || {},
offlineCountryConfiguration,
draft.data
draft.data,
userDetails
)

tempItem = field.previewGroup
Expand Down
4 changes: 2 additions & 2 deletions packages/config/src/handlers/forms/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,12 @@ const REQUIRED_FIELDS_IN_SECTION: Record<string, string[] | undefined> = {
],
deathEvent: [
'deathDate',
'placeOfDeathTitle',
'placeOfDeath',
'deathLocation',
...REQUIRED_EVENT_ADDRESS_FIELDS.map((field) => `${field}Placeofdeath`)
],
marriageEvent: [
'marriageDate',
'placeOfMarriageTitle',
...REQUIRED_EVENT_ADDRESS_FIELDS.map((field) => `${field}Placeofmarriage`)
],
groom: [
Expand Down Expand Up @@ -285,10 +283,12 @@ const OPTIONAL_FIELDS_IN_SECTION: Record<string, string[] | undefined> = {
'causeOfDeathEstablished',
'causeOfDeathMethod',
'deathDescription',
'placeOfDeathTitle',
...OPTIONAL_EVENT_ADDRESS_FIELDS.map((field) => `${field}Placeofdeath`)
],
marriageEvent: [
'typeOfMarriage',
'placeOfMarriageTitle',
...OPTIONAL_EVENT_ADDRESS_FIELDS.map((field) => `${field}Placeofmarriage`)
],
groom: [
Expand Down
29 changes: 18 additions & 11 deletions packages/login/src/common/LoginBackgroundWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
import * as React from 'react'
import styled, { css, useTheme } from 'styled-components'

import { storage } from '@login/storage'
import {
selectCountryBackground,
selectCountryLogo
Expand Down Expand Up @@ -57,29 +57,36 @@ export interface IProps {

export function usePersistentCountryBackground() {
const theme = useTheme()
const countryBackground: NonNullable<
ReturnType<typeof selectCountryBackground>
> = JSON.parse(
localStorage.getItem('country-background') ??
`{"backgroundColor" : "${(theme as ITheme).colors.backgroundPrimary}"}`
)
const [countryBackground, setCountryBackground] = React.useState({
backgroundColor: `${(theme as ITheme).colors.backgroundPrimary}`,
backgroundImage: '',
imageFit: ''
})
const [offlineBackground, setOfflineBackground] =
React.useState(countryBackground)
React.useEffect(() => {
storage.getItem('country-background').then((res) => {
res ?? setCountryBackground(JSON.parse(res))
})
}, [])

const background = useSelector(selectCountryBackground)

if (background && !isEqual(background, offlineBackground)) {
setOfflineBackground(background)
localStorage.setItem('country-background', JSON.stringify(background))
storage.setItem('country-background', JSON.stringify(background))
}

return offlineBackground
}

export function usePersistentCountryLogo() {
const [offlineLogo, setOfflineLogo] = React.useState(
localStorage.getItem('country-logo') ?? ''
)
const [offlineLogo, setOfflineLogo] = React.useState('')
React.useEffect(() => {
storage.getItem('country-logo').then((res) => {
res ?? setOfflineLogo(res)
})
}, [])
const logo = useSelector(selectCountryLogo)
if (logo && logo !== offlineLogo) {
setOfflineLogo(logo)
Expand Down
14 changes: 12 additions & 2 deletions packages/login/src/login/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
import { LoginState } from '@login/login/reducer'
import { IStoreState } from '@login/store'
import { ILoginBackground } from '@login/utils/authApi'
import * as React from 'react'
import { useSelector } from 'react-redux'
const getPartialState = (store: IStoreState): LoginState => store.login
Expand All @@ -36,16 +37,25 @@ export function selectCountryLogo(store: IStoreState) {
return getKey(store, 'config').COUNTRY_LOGO?.file
}

export function selectCountryBackground(store: IStoreState) {
export function selectCountryBackground(store: IStoreState): ILoginBackground {
const countryBackground = getKey(store, 'config').LOGIN_BACKGROUND
if (countryBackground?.backgroundImage) {
return {
backgroundColor: '',
backgroundImage: countryBackground.backgroundImage,
imageFit: countryBackground.imageFit
}
} else if (countryBackground?.backgroundColor) {
return {
backgroundColor: countryBackground?.backgroundColor
backgroundColor: countryBackground?.backgroundColor,
backgroundImage: '',
imageFit: ''
}
} else {
return {
backgroundColor: '',
backgroundImage: '',
imageFit: ''
}
}
}
Expand Down
Loading