Skip to content

Commit

Permalink
Merge pull request #558 from ChildMindInstitute/release/v2.5.2
Browse files Browse the repository at this point in the history
Release/v2.5.2 [main]
  • Loading branch information
mbanting authored Nov 27, 2024
2 parents 908a21e + e7a48e4 commit a7b1082
Show file tree
Hide file tree
Showing 23 changed files with 2,120 additions and 1,977 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@types/mixpanel-browser": "^2.47.5",
"@types/node": "^20.10.4",
"@uiw/react-md-editor": "^3.20.5",
"axios": "^1.2.1",
"axios": "^1.7.7",
"buffer": "^6.0.3",
"date-fns": "^2.29.3",
"dayspan": "^1.1.0",
Expand Down Expand Up @@ -82,7 +82,7 @@
"husky": "^8.0.2",
"jest": "^29.7.0",
"jest-canvas-mock": "^2.5.2",
"jsdom": "^23.2.0",
"jsdom": "^25.0.1",
"node-stdlib-browser": "^1.2.0",
"prettier": "^3.2.2",
"typescript": "^4.6.4",
Expand Down
1 change: 1 addition & 0 deletions src/abstract/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const phrasalTemplateCompatibleResponseTypes = [
'singleSelect',
'slider',
'text',
'paragraphText',
'time',
'timeRange',
'multiSelectRows',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const useTextVariablesReplacer = ({
}: Props) => {
const replaceTextVariables = (text: string) => {
if (items && answers) {
const nickname = respondentMeta?.nickname;
const nickname = respondentMeta?.nickname ?? '';

const replacer = new MarkdownVariableReplacer(items, answers, completedEntityTime, nickname);
return replacer.process(text);
Expand Down
2 changes: 1 addition & 1 deletion src/entities/activity/ui/ActivityCardItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMemo } from 'react';

import { AdditionalTextResponse } from './AdditionalTextResponse';
import { ItemPicker } from './items/ItemPicker';
import { Answer, hasAdditionalResponse, requiresAdditionalResponse } from '../lib';
import { ItemPicker } from './items/ItemPicker';

import { appletModel } from '~/entities/applet';
import { SliderAnimation } from '~/shared/animations';
Expand Down
180 changes: 50 additions & 130 deletions src/entities/activity/ui/items/ActionPlan/Document.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import React, { useEffect, useState, useContext, useMemo, useCallback } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';

import { Box } from '@mui/material';
import { v4 as uuidV4 } from 'uuid';

import { DocumentContext } from './DocumentContext';
import { useAvailableBodyWidth, useCorrelatedPageMaxHeightLineCount } from './hooks';
import { Page } from './Page';
import {
DocumentContext,
DocumentData,
IdentifiablePhrasalTemplatePhrase,
PageComponent,
} from './Document.type';
import { useAvailableBodyWidth, usePageMaxHeight } from './hooks';
import { buildDocumentData, buildPageComponents } from './pageComponent';
import { pageRenderer, pagesRenderer } from './pageRenderer';
import { extractActivitiesPhrasalData } from './phrasalData';

import { getProgressId } from '~/abstract/lib';
import { PhrasalTemplatePhrase } from '~/entities/activity/lib';
import { useActionPlanTranslation } from '~/entities/activity/lib/useActionPlanTranslation';
import { appletModel } from '~/entities/applet';
import { SurveyContext } from '~/features/PassSurvey';
import { useAppSelector } from '~/shared/utils';
import measureComponentHeight from '~/shared/utils/measureComponentHeight';
import { useAppSelector, useOnceEffect } from '~/shared/utils';

type DocumentProps = {
documentId: string;
Expand All @@ -22,29 +28,15 @@ type DocumentProps = {
phrasalTemplateCardTitle: string;
};

type IdentifiablePhrasalTemplatePhrase = PhrasalTemplatePhrase & { id: string };

export const Document = ({
documentId,
appletTitle,
phrases,
phrasalTemplateCardTitle,
}: DocumentProps) => {
const { t } = useActionPlanTranslation();
const context = useContext(SurveyContext);

const noImage = useMemo(() => phrases.filter((phrase) => !!phrase.image).length <= 0, [phrases]);

const identifiablePhrases = useMemo(
() =>
phrases.map<IdentifiablePhrasalTemplatePhrase>((phrase) => {
return {
...phrase,
id: uuidV4(),
};
}),
[phrases],
);

const activityProgress = useAppSelector((state) =>
appletModel.selectors.selectActivityProgress(
state,
Expand All @@ -57,120 +49,48 @@ export const Document = ({
[activityProgress],
);

const identifiablePhrases = useMemo(
() => phrases.map<IdentifiablePhrasalTemplatePhrase>((phrase) => ({ ...phrase, id: uuidV4() })),
[phrases],
);

const documentData = useMemo<DocumentData>(
() => buildDocumentData(identifiablePhrases),
[identifiablePhrases],
);

const pageComponents = useMemo<PageComponent[]>(
() => buildPageComponents(t, activitiesPhrasalData, identifiablePhrases),
[t, activitiesPhrasalData, identifiablePhrases],
);

const availableWidth = useAvailableBodyWidth();
const correlatedPageMaxHeightLineCount = useCorrelatedPageMaxHeightLineCount();
const pageMaxHeight = correlatedPageMaxHeightLineCount.maxHeight;
const pageMaxHeight = usePageMaxHeight();
const [pages, setPages] = useState<React.ReactNode[]>([]);

const renderPages = useCallback(async () => {
const renderedPages: React.ReactNode[] = [];

const renderPage = async (
pagePhrases: IdentifiablePhrasalTemplatePhrase[],
): Promise<[React.ReactNode, IdentifiablePhrasalTemplatePhrase[]]> => {
const curPageNumber = renderedPages.length + 1;

const curPage = (
<Page
key={`page-${curPageNumber}`}
documentId={documentId}
pageNumber={curPageNumber}
appletTitle={appletTitle}
phrasalTemplateCardTitle={phrasalTemplateCardTitle}
phrases={pagePhrases}
phrasalData={activitiesPhrasalData}
noImage={noImage}
/>
);

const pageHeight = await measureComponentHeight(availableWidth, curPage);
if (pageHeight <= pageMaxHeight) {
// If the rendered page fits into the maximum allowed page height,
// then stop rendering.
return [curPage, []];
}

if (pagePhrases.length <= 1) {
const pagePhrase = pagePhrases[0];
const pagePhraseFields = pagePhrase.fields;

if (pagePhraseFields.length <= 1) {
// If the rendered page does not fit into the maximum allowed page
// height, and there is only 1 phrase for the page, but that phrase
// has on 1 field (this means there is nothing left to split), then
// stop rendering.
return [curPage, []];
}

// If the rendered page does not fit into the maximum allowed page
// height, and there is only 1 phrase for the page, and that phrase
// has more than 1 field, then split the fields into multiple phrases
// with the same ID and re-render.
const splits: [IdentifiablePhrasalTemplatePhrase, IdentifiablePhrasalTemplatePhrase] = [
{
id: pagePhrase.id,
image: pagePhrase.image,
fields: pagePhraseFields.slice(0, pagePhraseFields.length - 1),
},
{
id: pagePhrase.id,
image: pagePhrase.image,
fields: pagePhraseFields.slice(pagePhraseFields.length - 1),
},
];

const [newPage, newPageRestPhrases] = await renderPage([splits[0]]);
const leftoverPhrases = [...newPageRestPhrases, splits[1]];
return [newPage, leftoverPhrases];
}

// If the rendered page does not fit into the maximum allowed page
// height, and the page has more than 1 phrase, then split the phrases
// and re-render.
const newPagePhrases = pagePhrases.slice(0, pagePhrases.length - 1);
const curPageRestPhrases = pagePhrases.slice(pagePhrases.length - 1);
const [newPage, newPageRestPhrases] = await renderPage(newPagePhrases);
const leftoverPhrases = [...newPageRestPhrases, ...curPageRestPhrases];

const recombinedLeftoverPhrases = leftoverPhrases.reduce((acc, phrase) => {
const existingPhrase = acc.find(({ id }) => id === phrase.id);
if (existingPhrase) {
existingPhrase.fields = [...existingPhrase.fields, ...phrase.fields];
} else {
acc.push(phrase);
}
return acc;
}, [] as IdentifiablePhrasalTemplatePhrase[]);

return [newPage, recombinedLeftoverPhrases];
};

const _renderPages = async (_pagePhrases: IdentifiablePhrasalTemplatePhrase[]) => {
const [renderedPage, leftoverPhrases] = await renderPage(_pagePhrases);
renderedPages.push(renderedPage);

if (leftoverPhrases.length > 0) {
await _renderPages(leftoverPhrases);
}
};

await _renderPages(identifiablePhrases);
const renderOnePage = useMemo(
() =>
pageRenderer(availableWidth, {
documentId,
documentData,
appletTitle,
phrasalTemplateCardTitle,
}),
[appletTitle, availableWidth, documentData, documentId, phrasalTemplateCardTitle],
);

const renderMorePage = useMemo(
() => pagesRenderer(renderOnePage, pageMaxHeight),
[pageMaxHeight, renderOnePage],
);

const renderAllPages = useCallback(async () => {
const renderedPages = await renderMorePage(1, pageComponents);
setPages(renderedPages);
}, [
documentId,
activitiesPhrasalData,
appletTitle,
availableWidth,
pageMaxHeight,
phrasalTemplateCardTitle,
identifiablePhrases,
noImage,
]);

useEffect(() => {
void renderPages();
}, [renderPages]);
}, [pageComponents, renderMorePage]);
useOnceEffect(() => {
void renderAllPages();
});

return (
<Box
Expand Down
78 changes: 78 additions & 0 deletions src/entities/activity/ui/items/ActionPlan/Document.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

import { PhrasalTemplatePhrase } from '~/entities/activity';

export type IdentifiablePhrasalTemplatePhrase = PhrasalTemplatePhrase & { id: string };

// This should be kept in a separate file from `Document` because both `Document` and `Page` need
// this context, but `Document` also needs `Page`. So keeping this context in this separate file
// avoids a circular import path.
export const DocumentContext = React.createContext<{
totalPages: number;
}>({
totalPages: 0,
});

export type FieldValueTransformer = (value: string) => string;

export type FieldValueItemsJoiner = (values: string[]) => string;

type BasePageComponent = {
phraseIndex: number;
phraseId: string;
};

export type SentencePageComponent = BasePageComponent & {
componentType: 'sentence';
text: string;
};

type BaseItemResponsePageComponent = BasePageComponent & {
componentType: 'item_response';
};

export type ListItemResponsePageComponent = BaseItemResponsePageComponent & {
componentType: 'item_response';
itemResponseType: 'list';
items: string[];
};

export type TextItemResponsePageComponent = BaseItemResponsePageComponent & {
componentType: 'item_response';
itemResponseType: 'text';
text: string;
};

type ItemResponsePageComponent = ListItemResponsePageComponent | TextItemResponsePageComponent;

export type LineBreakPageComponent = BasePageComponent & {
componentType: 'line_break';
};

/**
* Newline is a special component that is used to force a new line in the document, used when
* rendering the `paragraphText` item type.
*/
export type NewlinePageComponent = BasePageComponent & {
componentType: 'newline';
};

export type PageComponent =
| SentencePageComponent
| ItemResponsePageComponent
| LineBreakPageComponent
| NewlinePageComponent;

export type DocumentData = {
imageUrlByPhraseId: Record<string, string>;
hasImage: boolean;
};

export type FlatComponentIndex = [number] | [number, number];

export type PageRenderer = (
pageNumber: number,
components: PageComponent[],
flatIndices: FlatComponentIndex[],
inclusivePivot: number,
) => Promise<{ page: React.ReactNode; pageHeight: number; restComponents: PageComponent[] }>;
7 changes: 0 additions & 7 deletions src/entities/activity/ui/items/ActionPlan/DocumentContext.ts

This file was deleted.

Loading

0 comments on commit a7b1082

Please sign in to comment.