diff --git a/.github/workflows/build_test_lint.yml b/.github/workflows/build_test_lint.yml index 827c91a0a..0a52cf114 100644 --- a/.github/workflows/build_test_lint.yml +++ b/.github/workflows/build_test_lint.yml @@ -94,34 +94,3 @@ jobs: - name: Check formatting run: npm run check-formatting - - deploy-storybook: - name: Deploy Storybook to S3 - runs-on: ubuntu-latest - permissions: - contents: read - pages: write - id-token: write - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: npm - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::209248795938:role/SmartFormsReactAppDeployment - aws-region: ap-southeast-2 - - - name: Install dependencies - run: npm ci - - - name: Build application - run: npm run build-storybook -w packages/smart-forms-renderer - - - name: Upload static Storybook site to S3 - run: aws s3 sync packages/smart-forms-renderer/storybook-static s3://smart-forms-storybook/storybook diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 96f0c6d3e..ebc19e2d2 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -1,7 +1,9 @@ -name: Smart Forms Docs Deployment Workflow +name: Smart Forms Docs + Storybook Deployment Workflow on: push: + branches-ignore: + - alpha permissions: contents: read @@ -35,3 +37,34 @@ jobs: - name: Upload static Docusaurus site to S3 run: aws s3 sync documentation/build s3://smart-forms-docs/docs --cache-control no-cache + + deploy-storybook-s3: + name: Deploy Storybook to S3 + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::209248795938:role/SmartFormsReactAppDeployment + aws-region: ap-southeast-2 + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build-storybook -w packages/smart-forms-renderer + + - name: Upload static Storybook site to S3 + run: aws s3 sync packages/smart-forms-renderer/storybook-static s3://smart-forms-storybook/storybook diff --git a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItem.tsx index 86aac00bc..15a3ecbf5 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItem.tsx @@ -39,6 +39,7 @@ interface GroupItemProps qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; groupCardElevation: number; + disableCardView?: boolean; tabIsMarkedAsComplete?: boolean; tabs?: Tabs; currentTabIndex?: number; @@ -53,6 +54,7 @@ function GroupItem(props: GroupItemProps) { qrItem, isRepeated, groupCardElevation, + disableCardView, tabIsMarkedAsComplete, tabs, currentTabIndex, @@ -103,6 +105,7 @@ function GroupItem(props: GroupItemProps) { qrItemsByIndex={qrItemsByIndex} isRepeated={isRepeated} groupCardElevation={groupCardElevation} + disableCardView={disableCardView} tabIsMarkedAsComplete={tabIsMarkedAsComplete} tabs={tabs} currentTabIndex={currentTabIndex} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx index 920cdffdb..88ec8b5bb 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx @@ -50,6 +50,7 @@ interface GroupItemViewProps childQItems: QuestionnaireItem[]; qrItemsByIndex: (QuestionnaireResponseItem | QuestionnaireResponseItem[] | undefined)[]; groupCardElevation: number; + disableCardView?: boolean; tabIsMarkedAsComplete?: boolean; tabs?: Tabs; currentTabIndex?: number; @@ -65,6 +66,7 @@ function GroupItemView(props: GroupItemViewProps) { qrItemsByIndex, isRepeated, groupCardElevation, + disableCardView, tabIsMarkedAsComplete, tabs, currentTabIndex, @@ -133,6 +135,37 @@ function GroupItemView(props: GroupItemViewProps) { ); } + // Disable card view - currently only available via disablePageCardView API + if (disableCardView) { + return ( + + {childQItems.map((qItem: QuestionnaireItem, i) => { + const qrItemOrItems = qrItemsByIndex[i]; + + return ( + + ); + })} + {/* Next tab button at the end of each tab group */} + + + + ); + } + return ( isPage(i)); - if (everyItemIsPage) { + if (formIsPaginated) { return ( - = useMemo( - () => mapQItemsIndex(topLevelQItem), - [topLevelQItem] - ); - - const nonNullTopLevelQRItem = topLevelQRItem ?? createEmptyQrGroup(topLevelQItem); - - const qItems = topLevelQItem.item; - const qrItems = nonNullTopLevelQRItem.item; - - function handleQrGroupChange(qrItem: QuestionnaireResponseItem) { - updateQrItemsInGroup(qrItem, null, nonNullTopLevelQRItem, indexMap); - onQrItemChange(nonNullTopLevelQRItem); - } - - if (!qItems || !qrItems) { - return <>Unable to load form; - } - - const qrItemsByIndex = getQrItemsIndex(qItems, qrItems, indexMap); - - return ( - - - - {qItems.map((qItem, i) => { - const qrItem = qrItemsByIndex[i]; - - const isNotRepeatGroup = !Array.isArray(qrItem); - const isPage = !!pages[qItem.linkId]; - - if (!isPage || !isNotRepeatGroup) { - // Something has gone horribly wrong - return null; - } - - const isRepeated = qItem.repeats ?? false; - const pageIsMarkedAsComplete = pages[qItem.linkId].isComplete ?? false; - - return ( - - - - ); - })} - - - - ); -} - -export default FormBodyPage; diff --git a/packages/smart-forms-renderer/src/components/Renderer/FormBodyPaginated.tsx b/packages/smart-forms-renderer/src/components/Renderer/FormBodyPaginated.tsx new file mode 100644 index 000000000..ee867b96e --- /dev/null +++ b/packages/smart-forms-renderer/src/components/Renderer/FormBodyPaginated.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import Grid from '@mui/material/Grid'; +import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import TabContext from '@mui/lab/TabContext'; +import TabPanel from '@mui/lab/TabPanel'; +import GroupItem from '../FormComponents/GroupItem/GroupItem'; +import type { + PropsWithParentIsReadOnlyAttribute, + PropsWithQrItemChangeHandler +} from '../../interfaces/renderProps.interface'; +import { useQuestionnaireStore } from '../../stores'; +import { useRendererStylingStore } from '../../stores/rendererStylingStore'; +import { SingleItem } from '../FormComponents'; +import PageButtonsWrapper from '../FormComponents/GroupItem/PageButtonWrapper'; +import { QGroupContainerBox } from '../Box.styles'; +import { GroupCard } from '../FormComponents/GroupItem/GroupItem.styles'; + +interface FormBodyPaginatedProps + extends PropsWithQrItemChangeHandler, + PropsWithParentIsReadOnlyAttribute { + topLevelQItems: QuestionnaireItem[]; + topLevelQRItems: (QuestionnaireResponseItem | QuestionnaireResponseItem[] | undefined)[]; +} + +// TODO This implementation doesnt take into account repeat items and repeat groups +// TODO need to fix this before bringing it into release +// Every group item in here is rendered as a page +function FormBodyPaginated(props: FormBodyPaginatedProps) { + const { topLevelQItems, topLevelQRItems, parentIsReadOnly, onQrItemChange } = props; + + const pages = useQuestionnaireStore.use.pages(); + const currentPage = useQuestionnaireStore.use.currentPageIndex(); + const disableCardView = useRendererStylingStore.use.disablePageCardView(); + + return ( + + + + {topLevelQItems.map((qItem, i) => { + const qrItem = topLevelQRItems[i]; + + const isNotRepeatGroup = !Array.isArray(qrItem); + const isPage = !!pages[qItem.linkId]; + + const itemIsGroup = qItem.type === 'group'; + + if (!isPage || !isNotRepeatGroup) { + // Something has gone horribly wrong + return null; + } + + const isRepeated = qItem.repeats ?? false; + const pageIsMarkedAsComplete = pages[qItem.linkId].isComplete ?? false; + + // Render this page as a group + if (itemIsGroup) { + return ( + + + + ); + } + + // Page consists of a non-group item + return ( + + + {disableCardView ? ( + <> + + + + ) : ( + + + + + )} + + + ); + })} + + + + ); +} + +export default FormBodyPaginated; diff --git a/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelItem.tsx b/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelItem.tsx index 4fcd3459c..392e38e72 100644 --- a/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelItem.tsx +++ b/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelItem.tsx @@ -18,9 +18,7 @@ import React from 'react'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import FormBodyTabbed from './FormBodyTabbed'; -import FormBodyPage from './FormBodyPage'; import { containsTabs, isTabContainer } from '../../utils/tabs'; -import { containsPages, isPage } from '../../utils/page'; import GroupItem from '../FormComponents/GroupItem/GroupItem'; import SingleItem from '../FormComponents/SingleItem/SingleItem'; import type { @@ -59,9 +57,6 @@ function FormTopLevelItem(props: FormTopLevelItemProps) { const itemIsTabContainer = isTabContainer(topLevelQItem); const itemContainsTabs = containsTabs(topLevelQItem); - const itemIsPageContainer = isPage(topLevelQItem); - const itemContainsPages = containsPages(topLevelQItem); - const isTablet = useResponsive('up', 'md'); const itemIsGroup = topLevelQItem.type === 'group'; @@ -115,18 +110,6 @@ function FormTopLevelItem(props: FormTopLevelItemProps) { ); } - if (itemContainsPages || itemIsPageContainer) { - return ( - - ); - } - // If form is untabbed, it is rendered as a regular group if (itemIsGroup) { // Item is 'grid' diff --git a/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelPage.tsx b/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelPage.tsx deleted file mode 100644 index b2ae07b32..000000000 --- a/packages/smart-forms-renderer/src/components/Renderer/FormTopLevelPage.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import Grid from '@mui/material/Grid'; -import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; -import TabContext from '@mui/lab/TabContext'; -import TabPanel from '@mui/lab/TabPanel'; -import GroupItem from '../FormComponents/GroupItem/GroupItem'; -import type { - PropsWithParentIsReadOnlyAttribute, - PropsWithQrItemChangeHandler -} from '../../interfaces/renderProps.interface'; -import { useQuestionnaireStore } from '../../stores'; - -interface FormTopLevelPageProps - extends PropsWithQrItemChangeHandler, - PropsWithParentIsReadOnlyAttribute { - topLevelQItems: QuestionnaireItem[]; - topLevelQRItems: (QuestionnaireResponseItem | QuestionnaireResponseItem[] | undefined)[]; -} - -function FormTopLevelPage(props: FormTopLevelPageProps) { - const { topLevelQItems, topLevelQRItems, parentIsReadOnly, onQrItemChange } = props; - - const pages = useQuestionnaireStore.use.pages(); - const currentPage = useQuestionnaireStore.use.currentPageIndex(); - - return ( - - - - {topLevelQItems.map((qItem, i) => { - const qrItem = topLevelQRItems[i]; - - const isNotRepeatGroup = !Array.isArray(qrItem); - const isPage = !!pages[qItem.linkId]; - - if (!isPage || !isNotRepeatGroup) { - // Something has gone horribly wrong - return null; - } - - const isRepeated = qItem.repeats ?? false; - const pageIsMarkedAsComplete = pages[qItem.linkId].isComplete ?? false; - - return ( - - - - ); - })} - - - - ); -} - -export default FormTopLevelPage; diff --git a/packages/smart-forms-renderer/src/hooks/useBuildForm.ts b/packages/smart-forms-renderer/src/hooks/useBuildForm.ts index ffaa79c62..e854bab14 100644 --- a/packages/smart-forms-renderer/src/hooks/useBuildForm.ts +++ b/packages/smart-forms-renderer/src/hooks/useBuildForm.ts @@ -15,10 +15,12 @@ * limitations under the License. */ -import { ComponentType, useLayoutEffect, useState } from 'react'; +import type { ComponentType } from 'react'; +import { useLayoutEffect, useState } from 'react'; import { buildForm } from '../utils'; import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -import { RendererStyling, useRendererStylingStore } from '../stores/rendererStylingStore'; +import type { RendererStyling } from '../stores/rendererStylingStore'; +import { useRendererStylingStore } from '../stores/rendererStylingStore'; /** * React hook wrapping around the buildForm() function to build a form from a questionnaire and an optional QuestionnaireResponse. @@ -29,7 +31,7 @@ import { RendererStyling, useRendererStylingStore } from '../stores/rendererStyl * @param readOnly - Applies read-only mode to all items in the form view * @param terminologyServerUrl - Terminology server url to fetch terminology. If not provided, the default terminology server will be used. (optional) * @param additionalVariables - Additional key-value pair of SDC variables `Record` for testing (optional) - * @param rendererStyling - Renderer styling to be applied to the form. See docs for styling options. (optional) + * @param rendererStylingOptions - Renderer styling to be applied to the form. See docs for styling options. (optional) * @param customComponents - FIXME add comment * * @author Sean Fong @@ -40,7 +42,7 @@ function useBuildForm( readOnly?: boolean, terminologyServerUrl?: string, additionalVariables?: Record, - rendererStyling?: RendererStyling, + rendererStylingOptions?: RendererStyling, customComponents?: Record> ) { const [isBuilding, setIsBuilding] = useState(true); @@ -49,8 +51,8 @@ function useBuildForm( useLayoutEffect(() => { // Set optional renderer styling - if (rendererStyling) { - setRendererStyling(rendererStyling); + if (rendererStylingOptions) { + setRendererStyling(rendererStylingOptions); } buildForm( @@ -63,7 +65,16 @@ function useBuildForm( ).then(() => { setIsBuilding(false); }); - }, [additionalVariables, questionnaire, questionnaireResponse, readOnly, terminologyServerUrl]); + }, [ + customComponents, + questionnaire, + questionnaireResponse, + readOnly, + rendererStylingOptions, + setRendererStyling, + terminologyServerUrl, + additionalVariables + ]); return isBuilding; } diff --git a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts index baa7011fc..93f00e5a9 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts @@ -48,8 +48,8 @@ import { questionnaireResponseStore } from './questionnaireResponseStore'; import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems'; import { insertCompleteAnswerOptionsIntoQuestionnaire } from '../utils/questionnaireStoreUtils/insertAnswerOptions'; import type { InitialExpression } from '../interfaces/initialExpression.interface'; -import React from 'react'; -import { CustomComponentProps } from '../interfaces/customComponent.interface'; +import type { CustomComponentProps } from '../interfaces'; +import type { ComponentType } from 'react'; /** * QuestionnaireStore properties and methods @@ -118,7 +118,7 @@ export interface QuestionnaireStoreType { cachedValueSetCodings: Record; fhirPathContext: Record; populatedContext: Record; - customComponents: Record>; + customComponents: Record>; focusedLinkId: string; readOnly: boolean; buildSourceQuestionnaire: ( @@ -127,7 +127,7 @@ export interface QuestionnaireStoreType { additionalVariables?: Record, terminologyServerUrl?: string, readOnly?: boolean, - customComponents?: Record> + customComponents?: Record> ) => Promise; destroySourceQuestionnaire: () => void; switchTab: (newTabIndex: number) => void; diff --git a/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts b/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts index 367278501..651586305 100644 --- a/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts +++ b/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts @@ -30,6 +30,8 @@ export interface RendererStyling { | '800' | '900' | 'default'; + disablePageCardView?: boolean; + disablePageButtons?: boolean; } /** @@ -49,6 +51,8 @@ export interface RendererStylingStoreType { | '800' | '900' | 'default'; + disablePageCardView: boolean; + disablePageButtons: boolean; setRendererStyling: (params: RendererStyling) => void; } @@ -57,9 +61,13 @@ export interface RendererStylingStoreType { */ export const rendererStylingStore = createStore()((set) => ({ itemLabelFontWeight: 'default', + disablePageCardView: false, + disablePageButtons: false, setRendererStyling: (params: RendererStyling) => { set(() => ({ - itemLabelFontWeight: params.itemLabelFontWeight ?? 'default' + itemLabelFontWeight: params.itemLabelFontWeight ?? 'default', + disablePageCardView: params.disablePageCardView ?? false, + disablePageButtons: params.disablePageButtons ?? false })); } })); diff --git a/packages/smart-forms-renderer/src/utils/page.ts b/packages/smart-forms-renderer/src/utils/page.ts index 343dfde26..b3f4ccff0 100644 --- a/packages/smart-forms-renderer/src/utils/page.ts +++ b/packages/smart-forms-renderer/src/utils/page.ts @@ -45,8 +45,6 @@ export function getFirstVisiblePage( export function everyIsPages(topLevelQItem: QuestionnaireItem[] | undefined): boolean { if (!topLevelQItem) return false; - if (isPageContainer(topLevelQItem)) return false; - return topLevelQItem.every((i: QuestionnaireItem) => isPage(i)); } @@ -87,16 +85,11 @@ export function isPage(item: QuestionnaireItem) { * * @author Riza Nafis */ -export function constructPagesWithProperties( - qItems: QuestionnaireItem[] | undefined, - hasPageContainer: boolean -): Pages { +export function constructPagesWithProperties(qItems: QuestionnaireItem[] | undefined): Pages { if (!qItems) return {}; - const qItemPages = hasPageContainer ? qItems : qItems.filter(isPage); - const pages: Pages = {}; - for (const [i, qItem] of qItemPages.entries()) { + for (const [i, qItem] of qItems.entries()) { pages[qItem.linkId] = { pageIndex: i, isComplete: false, @@ -106,7 +99,7 @@ export function constructPagesWithProperties( return pages; } -interface contructPagesWithVisibilityParams { +interface constructPagesWithVisibilityParams { pages: Pages; enableWhenIsActivated: boolean; enableWhenItems: EnableWhenItems; @@ -114,7 +107,7 @@ interface contructPagesWithVisibilityParams { } export function constructPagesWithVisibility( - params: contructPagesWithVisibilityParams + params: constructPagesWithVisibilityParams ): { linkId: string; isVisible: boolean }[] { const { pages, enableWhenIsActivated, enableWhenItems, enableWhenExpressions } = params; diff --git a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractPages.ts b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractPages.ts index d6a29f60e..1e353a881 100644 --- a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractPages.ts +++ b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractPages.ts @@ -1,24 +1,17 @@ import type { Questionnaire } from 'fhir/r4'; import type { Pages } from '../../interfaces/page.interface'; -import { constructPagesWithProperties, isPage, isPageContainer } from '../page'; +import { constructPagesWithProperties, isPage } from '../page'; export function extractPages(questionnaire: Questionnaire): Pages { if (!questionnaire.item || questionnaire.item.length === 0) { return {}; } - if (!isPageContainer(questionnaire.item)) { - return constructPagesWithProperties(questionnaire.item, false); - } - - let totalPages = {}; - for (const topLevelItem of questionnaire.item) { - const items = topLevelItem.item; - const topLevelItemIsPageContainer = isPage(topLevelItem); + const questionnaireHasPage = questionnaire.item.some((i) => isPage(i)); - const pages = constructPagesWithProperties(items, topLevelItemIsPageContainer); - totalPages = { ...totalPages, ...pages }; + if (questionnaireHasPage) { + return constructPagesWithProperties(questionnaire.item); } - return totalPages; + return {}; }