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 {};
}