diff --git a/.prettierignore b/.prettierignore index c2c8d3e54..32f77635c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,9 @@ lib dist coverage +build *.md -*.stories.* \ No newline at end of file +*.stories.* +*.cjs +*.js +*.config.* diff --git a/apps/smart-forms-app/package.json b/apps/smart-forms-app/package.json index 2824fe60f..8b2699b80 100644 --- a/apps/smart-forms-app/package.json +++ b/apps/smart-forms-app/package.json @@ -27,7 +27,7 @@ "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", "@aehrc/sdc-populate": "^2.2.0", - "@aehrc/smart-forms-renderer": "^0.34.0", + "@aehrc/smart-forms-renderer": "^0.34.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", diff --git a/documentation/package.json b/documentation/package.json index 693249281..6c1ccded6 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc" }, "dependencies": { - "@aehrc/smart-forms-renderer": "^0.34.0", + "@aehrc/smart-forms-renderer": "^0.34.2", "@docusaurus/core": "3.3.2", "@docusaurus/preset-classic": "3.3.2", "@docusaurus/theme-live-codeblock": "^3.3.2", diff --git a/documentation/src/css/custom.css b/documentation/src/css/custom.css index ca4d1a7fc..82e624c00 100644 --- a/documentation/src/css/custom.css +++ b/documentation/src/css/custom.css @@ -30,7 +30,7 @@ } /* Custom CSS for code blocks */ -code, pre { +code, +pre { font-size: 0.875em; /* Adjust the font size as needed */ } - diff --git a/package-lock.json b/package-lock.json index ff1645b1b..f78b43fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", "@aehrc/sdc-populate": "^2.2.0", - "@aehrc/smart-forms-renderer": "^0.34.0", + "@aehrc/smart-forms-renderer": "^0.34.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", @@ -465,7 +465,7 @@ "name": "@aehrc/smart-forms-documentation", "version": "0.0.0", "dependencies": { - "@aehrc/smart-forms-renderer": "^0.34.0", + "@aehrc/smart-forms-renderer": "^0.34.2", "@docusaurus/core": "3.3.2", "@docusaurus/preset-classic": "3.3.2", "@docusaurus/theme-live-codeblock": "^3.3.2", @@ -43947,7 +43947,7 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "0.34.0", + "version": "0.34.2", "license": "Apache-2.0", "dependencies": { "@aehrc/sdc-populate": "^2.2.0", diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/createFhirPathContext.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/createFhirPathContext.ts index dfdda5434..3ef2b21d6 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/createFhirPathContext.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/createFhirPathContext.ts @@ -235,7 +235,9 @@ function createReferenceContextTuple( referenceContext, Promise.resolve( createWarningIssue( - `Reference Context ${referenceContext.part[0]?.valueString ?? ''} does not contain a reference` + `Reference Context ${ + referenceContext.part[0]?.valueString ?? '' + } does not contain a reference` ) ), null @@ -259,7 +261,9 @@ function createResourceContextTuple( resourceContext, Promise.resolve( createWarningIssue( - `${resourceContextName} bundle entry ${bundleEntry.fullUrl ?? ''} does not contain a request` + `${resourceContextName} bundle entry ${ + bundleEntry.fullUrl ?? '' + } does not contain a request` ) ), null diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index 18fa3908a..ef6304260 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/smart-forms-renderer", - "version": "0.34.0", + "version": "0.34.2", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx index db39eaf60..22b0f0c0e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx @@ -16,7 +16,11 @@ */ import React from 'react'; -import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import type { + QuestionnaireItem, + QuestionnaireItemAnswerOption, + QuestionnaireResponseItem +} from 'fhir/r4'; import { findInAnswerOptions, getChoiceControlType, getQrChoiceValue } from '../../../utils/choice'; import { createEmptyQrItem } from '../../../utils/qrItem'; import type { @@ -68,15 +72,25 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { }); // Event handlers - function handleChange(newValue: string) { - if (options.length === 0) { + function handleChange(newValue: QuestionnaireItemAnswerOption | string | null) { + // No options present or newValue is type null + if (options.length === 0 || newValue === null) { onQrItemChange(createEmptyQrItem(qItem)); return; } - const qrAnswer = findInAnswerOptions(options, newValue); + // newValue is type string + if (typeof newValue === 'string') { + const qrAnswer = findInAnswerOptions(options, newValue); + onQrItemChange( + qrAnswer ? { ...createEmptyQrItem(qItem), answer: [qrAnswer] } : createEmptyQrItem(qItem) + ); + return; + } + + // newValue is type QuestionnaireItemAnswerOption onQrItemChange( - qrAnswer ? { ...createEmptyQrItem(qItem), answer: [qrAnswer] } : createEmptyQrItem(qItem) + newValue ? { ...createEmptyQrItem(qItem), answer: [newValue] } : createEmptyQrItem(qItem) ); } @@ -117,7 +131,6 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { calcExpUpdated={calcExpUpdated} onFocusLinkId={() => onFocusLinkId(qItem.linkId)} onSelectChange={handleChange} - onClear={handleClear} /> ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx index 59601d1ec..01db9051a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx @@ -16,21 +16,22 @@ */ import React, { Fragment } from 'react'; -import InputAdornment from '@mui/material/InputAdornment'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; -import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; +import FadingCheckIcon from '../ItemParts/FadingCheckIcon'; +import Autocomplete from '@mui/material/Autocomplete'; +import { getAnswerOptionLabel } from '../../../utils/openChoice'; +import { compareAnswerOptionValue } from '../../../utils/choice'; interface ChoiceSelectAnswerOptionFieldsProps extends PropsWithIsTabledAttribute { qItem: QuestionnaireItem; options: QuestionnaireItemAnswerOption[]; - valueSelect: string; + valueSelect: QuestionnaireItemAnswerOption | null; readOnly: boolean; calcExpUpdated: boolean; - onSelectChange: (newValue: string) => void; + onSelectChange: (newValue: QuestionnaireItemAnswerOption | null) => void; } function ChoiceSelectAnswerOptionFields(props: ChoiceSelectAnswerOptionFieldsProps) { @@ -38,49 +39,39 @@ function ChoiceSelectAnswerOptionFields(props: ChoiceSelectAnswerOptionFieldsPro const { displayUnit, displayPrompt, entryFormat } = useRenderingExtensions(qItem); - // TODO use calcExpUpdated as updated feedback - return ( - + disabled={readOnly} + renderInput={(params) => ( + + {params.InputProps.endAdornment} + + {displayUnit} + + ) + }} + data-test="q-item-choice-dropdown-answer-value-set-field" + /> + )} + /> ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx index 4f9ea0275..1fd6ff42f 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx @@ -17,7 +17,11 @@ import React from 'react'; -import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import type { + QuestionnaireItem, + QuestionnaireItemAnswerOption, + QuestionnaireResponseItem +} from 'fhir/r4'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; import { createEmptyQrItem } from '../../../utils/qrItem'; import type { @@ -66,23 +70,28 @@ function ChoiceSelectAnswerOptionItem(props: ChoiceSelectAnswerOptionItemProps) } }); - // Event handlers - function handleChange(newValue: string) { - if (options.length === 0) { + function handleChange(newValue: QuestionnaireItemAnswerOption | string | null) { + // No options present or newValue is type null + if (options.length === 0 || newValue === null) { onQrItemChange(createEmptyQrItem(qItem)); return; } - const qrAnswer = findInAnswerOptions(options, newValue); + // newValue is type string + if (typeof newValue === 'string') { + const qrAnswer = findInAnswerOptions(options, newValue); + onQrItemChange( + qrAnswer ? { ...createEmptyQrItem(qItem), answer: [qrAnswer] } : createEmptyQrItem(qItem) + ); + return; + } + + // newValue is type QuestionnaireItemAnswerOption onQrItemChange( - qrAnswer ? { ...createEmptyQrItem(qItem), answer: [qrAnswer] } : createEmptyQrItem(qItem) + newValue ? { ...createEmptyQrItem(qItem), answer: [newValue] } : createEmptyQrItem(qItem) ); } - function handleClear() { - onQrItemChange(createEmptyQrItem(qItem)); - } - return ( onFocusLinkId(qItem.linkId)} onSelectChange={handleChange} - onClear={handleClear} /> ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx index 38b86332d..f5dea9ad1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FullWidthFormComponentBox } from '../../Box.styles'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import type { @@ -23,6 +23,7 @@ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; +import { findInAnswerOptions } from '../../../utils/choice'; import ChoiceSelectAnswerOptionFields from './ChoiceSelectAnswerOptionFields'; interface ChoiceSelectAnswerOptionViewProps @@ -33,9 +34,8 @@ interface ChoiceSelectAnswerOptionViewProps valueChoice: string | null; readOnly: boolean; calcExpUpdated: boolean; - onSelectChange: (linkId: string) => void; + onSelectChange: (newValue: QuestionnaireItemAnswerOption | null) => void; onFocusLinkId: () => void; - onClear: () => void; } function ChoiceSelectAnswerOptionView(props: ChoiceSelectAnswerOptionViewProps) { @@ -48,16 +48,20 @@ function ChoiceSelectAnswerOptionView(props: ChoiceSelectAnswerOptionViewProps) readOnly, calcExpUpdated, onFocusLinkId, - onSelectChange, - onClear + onSelectChange } = props; + const valueSelect: QuestionnaireItemAnswerOption | null = useMemo( + () => findInAnswerOptions(options, valueChoice ?? '') ?? null, + [options, valueChoice] + ); + if (isRepeated) { return ( diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonForStorybook.tsx index d6c2bfe87..dfbd67a12 100644 --- a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonForStorybook.tsx @@ -21,6 +21,7 @@ import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { Box, IconButton, Tooltip } from '@mui/material'; import Iconify from '../../components/Iconify/Iconify'; import { buildForm } from '../../utils'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; interface BuildFormButtonProps { questionnaire: Questionnaire; @@ -31,7 +32,12 @@ function BuildFormButtonForStorybook(props: BuildFormButtonProps) { const { questionnaire, questionnaireResponse } = props; async function handleBuildForm() { - await buildForm(questionnaire, questionnaireResponse); + await buildForm( + questionnaire, + questionnaireResponse, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); } return ( diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonTesterWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonTesterWrapperForStorybook.tsx index c0761448d..10214dee8 100644 --- a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonTesterWrapperForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormButtonTesterWrapperForStorybook.tsx @@ -23,6 +23,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { RendererThemeProvider } from '../../theme'; import { useBuildForm, useRendererQueryClient } from '../../hooks'; import BuildFormButtonForStorybook from './BuildFormButtonForStorybook'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; interface BuildFormButtonTesterWrapperForStorybookProps { questionnaire: Questionnaire; @@ -46,7 +47,12 @@ function BuildFormButtonTesterWrapperForStorybook( const queryClient = useRendererQueryClient(); - const isBuilding = useBuildForm(questionnaire); + const isBuilding = useBuildForm( + questionnaire, + undefined, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); if (isBuilding) { return
Loading...
; diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormWrapperForStorybook.tsx index ddc6ea243..3ac62e3f2 100644 --- a/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormWrapperForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/BuildFormWrapperForStorybook.tsx @@ -23,6 +23,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import RendererThemeProvider from '../../theme/Theme'; import { useBuildForm } from '../../hooks'; import useRendererQueryClient from '../../hooks/useRendererQueryClient'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; interface BuildFormWrapperForStorybookProps { questionnaire: Questionnaire; @@ -33,7 +34,12 @@ function BuildFormWrapperForStorybook(props: BuildFormWrapperForStorybookProps) const { questionnaire, questionnaireResponse } = props; const queryClient = useRendererQueryClient(); - const isBuilding = useBuildForm(questionnaire, questionnaireResponse); + const isBuilding = useBuildForm( + questionnaire, + questionnaireResponse, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); if (isBuilding) { return
Loading...
; diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/FormValidationTesterWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/FormValidationTesterWrapperForStorybook.tsx index 39f869e30..d987842e0 100644 --- a/packages/smart-forms-renderer/src/stories/storybookWrappers/FormValidationTesterWrapperForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/FormValidationTesterWrapperForStorybook.tsx @@ -24,6 +24,7 @@ import { RendererThemeProvider } from '../../theme'; import { useBuildForm, useRendererQueryClient } from '../../hooks'; import { Grid } from '@mui/material'; import FormValidationViewerForStorybook from './FormValidationViewerForStorybook'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; interface FormValidationTesterWrapperForStorybookProps { questionnaire: Questionnaire; @@ -35,7 +36,12 @@ function FormValidationTesterWrapperForStorybook( ) { const { questionnaire, questionnaireResponse } = props; - const isBuilding = useBuildForm(questionnaire, questionnaireResponse); + const isBuilding = useBuildForm( + questionnaire, + questionnaireResponse, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); const queryClient = useRendererQueryClient(); diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/PrePopWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/PrePopWrapperForStorybook.tsx index 9e645cfd7..5ee3538be 100644 --- a/packages/smart-forms-renderer/src/stories/storybookWrappers/PrePopWrapperForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/PrePopWrapperForStorybook.tsx @@ -27,6 +27,7 @@ import PrePopButtonForStorybook from './PrePopButtonForStorybook'; import { populateQuestionnaire } from '@aehrc/sdc-populate'; import { fetchResourceCallback } from './populateCallbackForStorybook'; import { buildForm } from '../../utils'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; interface PrePopWrapperForStorybookProps { questionnaire: Questionnaire; @@ -75,7 +76,12 @@ function PrePopWrapperForStorybook(props: PrePopWrapperForStorybookProps) { const { populatedResponse } = populateResult; // Call to buildForm to pre-populate the QR which repaints the entire BaseRenderer view - await buildForm(questionnaire, populatedResponse); + await buildForm( + questionnaire, + populatedResponse, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); setIsPopulating(false); }); diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/globals.ts b/packages/smart-forms-renderer/src/stories/storybookWrappers/globals.ts new file mode 100644 index 000000000..b0ae048e4 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/globals.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const STORYBOOK_TERMINOLOGY_SERVER_URL = 'https://r4.ontoserver.csiro.au/fhir'; diff --git a/packages/smart-forms-renderer/src/utils/choice.ts b/packages/smart-forms-renderer/src/utils/choice.ts index 9a643a544..323c8fa2f 100644 --- a/packages/smart-forms-renderer/src/utils/choice.ts +++ b/packages/smart-forms-renderer/src/utils/choice.ts @@ -80,6 +80,34 @@ export function findInAnswerOptions( return; } +/** + * Compare answer option value with selected value via valueString, valueInteger, or valueCoding.code + * + * @author Sean Fong + */ +export function compareAnswerOptionValue( + option: QuestionnaireItemAnswerOption, + value: QuestionnaireItemAnswerOption +): boolean { + if (!value) { + return false; + } + + if (value.valueString) { + return option.valueString === value.valueString; + } + + if (value.valueInteger) { + return option.valueInteger === value.valueInteger; + } + + if (value.valueCoding && value.valueCoding.code) { + return option.valueCoding?.code === value.valueCoding.code; + } + + return false; +} + /** * Get choice control type based on certain criteria in choice items * @@ -105,22 +133,6 @@ export function getChoiceControlType(qItem: QuestionnaireItem) { return ChoiceItemControl.Select; } -/** - * Find and return corresponding coding from AnswerValueSet based on selected answer in form - * - * @author Sean Fong - */ -export function findInAnswerValueSetCodings( - codings: Coding[], - selected: string -): QuestionnaireResponseItemAnswer | undefined { - for (const coding of codings) { - if (selected === coding.code) { - return coding; - } - } -} - /** * Find and return string value from selected answer * diff --git a/packages/smart-forms-renderer/src/utils/itemControl.ts b/packages/smart-forms-renderer/src/utils/itemControl.ts index 47c9fc802..7b338373c 100644 --- a/packages/smart-forms-renderer/src/utils/itemControl.ts +++ b/packages/smart-forms-renderer/src/utils/itemControl.ts @@ -26,10 +26,18 @@ function hasDisplayCategory(qItem: QuestionnaireItem): boolean { ); } +function hasItemControl(qItem: QuestionnaireItem): boolean { + return !!qItem.extension?.some( + (extension: Extension) => + extension.url === 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl' + ); +} + // If all nested items are of type display and have itemControl, then they should not be rendered export function shouldRenderNestedItems(qItem: QuestionnaireItem): boolean { return !qItem.item?.every( - (childItem) => childItem.type === 'display' && hasDisplayCategory(childItem) + (childItem) => + childItem.type === 'display' && (hasDisplayCategory(childItem) || hasItemControl(childItem)) ); }