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

Release/v2.5.2 [main] #558

Merged
merged 5 commits into from
Nov 27, 2024
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: 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
Loading