Skip to content

Commit

Permalink
Merge pull request #347 from concord-consortium/186622903-report-item
Browse files Browse the repository at this point in the history
Implement tecrock-table report item, render table to string, inline CSS
  • Loading branch information
pjanik authored Jan 26, 2024
2 parents 2b779ae + b4bd26b commit b5a8171
Show file tree
Hide file tree
Showing 13 changed files with 695 additions and 375 deletions.
651 changes: 402 additions & 249 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 12 additions & 9 deletions packages/tecrock-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@
"@types/react-dom": "^17.0.11",
"@types/react-jsonschema-form": "^1.7.13",
"@types/semver": "^7.5.6",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"autoprefixer": "^10.4.16",
"css-loader": "^6.8.1",
"autoprefixer": "^10.4.17",
"css-loader": "^6.9.1",
"enzyme": "^3.11.0",
"eslint": "^8.56.0",
"eslint-plugin-eslint-comments": "^3.2.0",
Expand All @@ -84,14 +84,15 @@
"html-webpack-plugin": "^5.6.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"mini-css-extract-plugin": "^2.7.6",
"mini-css-extract-plugin": "^2.7.7",
"npm-run-all": "^4.1.5",
"postcss-loader": "^7.3.4",
"sass": "^1.69.7",
"raw-loader": "^4.0.2",
"sass": "^1.70.0",
"sass-loader": "^13.3.3",
"script-loader": "^0.7.2",
"style-loader": "^3.3.3",
"ts-jest": "^29.1.1",
"style-loader": "^3.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
Expand All @@ -107,8 +108,10 @@
"@concord-consortium/text-decorator": "^1.0.2",
"classnames": "^2.5.1",
"json-schema": "^0.4.0",
"juice": "^10.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-jsonschema-form": "^1.8.1"
"react-jsonschema-form": "^1.8.1",
"semver": "^7.5.4"
}
}
40 changes: 40 additions & 0 deletions packages/tecrock-table/src/components/report-item/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from "react";
import { useInitMessage, useAutoSetHeight, useReportItem } from "@concord-consortium/lara-interactive-api";
import { ITectonicExplorerInteractiveState } from "@concord-consortium/tecrock-shared";
import { reportItemHandler } from "./report-item";
import { IAuthoredState } from "../../types";

interface Props { }

export const AppComponent: React.FC<Props> = (props) => {
const initMessage = useInitMessage();

useAutoSetHeight();

useReportItem<ITectonicExplorerInteractiveState, IAuthoredState>({
metadata: {
compactAnswerReportItemsAvailable: true
},
handler: reportItemHandler
});

if (!initMessage) {
return (
<div>
Loading...
</div>
);
}

if (initMessage.mode !== "reportItem") {
return (
<div>
This interactive is only available in &apos;reportItem&apos; mode but &apos;{initMessage.mode}&apos; was given.
</div>
);
}

// Report item app can provide UI in the prompt area too, but TecRock Table never does it. It only responds
// to getReportItemAnswer post message from the host window (portal report).
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import * as ReactDOM from "react-dom";
import { inlineContent } from "juice";

export const renderToString = (element: React.ReactNode) => {
const div = document.createElement("div");
ReactDOM.render(element as any, div);
return div.innerHTML;
};

export const renderToStringWithCss = (element: React.ReactNode, css: string, classMap?: Record<string, string>) => {
const html = renderToString(element);
if (classMap) {
css = replaceCssClassNames(css, classMap);
}
return inlineContent(html, css);
};

// This function is useful to replace class names in a raw CSS string with new class names provided by CSS modules.
export const replaceCssClassNames = (cssString: string, classMap: Record<string, string>): string => {
// Create a regular expression that matches all keys in the classMap.
const classNamesRegex = new RegExp(`\\.(${Object.keys(classMap).join('|')})`, 'g');

return cssString.replace(classNamesRegex, (match) => {
// Extract the class name without the dot.
const className = match.substring(1);

// Replace with the new class name, re-adding the dot.
return `.${classMap[className]}`;
});
};
39 changes: 39 additions & 0 deletions packages/tecrock-table/src/components/report-item/report-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from "react";
import * as semver from "semver";
import { sendReportItemAnswer, IReportItemAnswerItem, IGetReportItemAnswerHandler } from "@concord-consortium/lara-interactive-api";
import { IAuthoredState } from "../../types";
import { ITectonicExplorerInteractiveState } from "@concord-consortium/tecrock-shared";
import { renderToStringWithCss } from "./render-to-string";

import { Table } from "../table";
// The `.inline` suffix will trigger a Webpack loader that simply converts SCSS to CSS and returns it as a string.
import inlineCss from "../table.inline.scss";
// Regular `.scss` files use CSS modules and return a map of class names to unique identifiers. This map is later
// used to replace class names in the `inlineCss` file.
import classMap from "../table.scss";

export const reportItemHandler: IGetReportItemAnswerHandler<ITectonicExplorerInteractiveState, IAuthoredState> = request => {
const {platformUserId, version, itemsType} = request;

if (!version) {
// for hosts sending older, unversioned requests
// tslint:disable-next-line:no-console
console.error("TecRock Table Report Item Interactive: Missing version in getReportItemAnswer request.");
}
else if (semver.satisfies(version, "2.x")) {
const items: IReportItemAnswerItem[] = [];
const htmlWithInlineStyles = renderToStringWithCss(
<Table interactiveState={request.interactiveState} />,
inlineCss,
classMap
);
items.push({type: "html", html: htmlWithInlineStyles});
sendReportItemAnswer({version, platformUserId, items, itemsType});
} else {
// tslint:disable-next-line:no-console
console.error(
"TecRock Table Report Item Interactive: Unsupported version in getReportItemAnswer request:",
version
);
}
};
64 changes: 0 additions & 64 deletions packages/tecrock-table/src/components/runtime.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,70 +21,6 @@
justify-content: center;
}

.tableAndSnapshots {
display: flex;
flex-direction: row;
margin-top: 10px;

.table {
overflow-x: auto;
margin-right: 10px;
vertical-align: top;

table {
border-collapse: collapse;
table-layout: auto;

td, th {
border: 0.5px solid #979797;
padding: 4px;
text-align: center;

&.temperatureAndPressure {
max-width: 100px;
}
&.type {
max-width: 200px;
}
}

td.notes {
min-width: 200px;
text-align: left;
}
}
}

.snapshots {
// This will result in flex-grow: 1 and ensure that the snapshots will be as wide as possible.
flex: 1;
min-width: 30%;
// Limit max size when snapshots are displayed in the report and user is using a big screen. In this case,
// image could become much bigger than its native size.
max-width: 1000px;

.imgContainer {
position: relative;
width: 100%;
margin-bottom: 5px;
cursor: pointer;

img {
width: 100%;
}

svg {
display: block;
position: absolute;
bottom: 3px;
right: 3px;
height: 30px;
pointer-events: none;
}
}
}
}

&.report {
overflow-x: auto;
.tableAndSnapshots {
Expand Down
53 changes: 4 additions & 49 deletions packages/tecrock-table/src/components/runtime.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useEffect, useCallback } from "react";
import classNames from "classnames";
import { IRuntimeQuestionComponentProps } from "@concord-consortium/question-interactives-helpers/src/components/base-question-app";
import { Table } from "./table";
import { IAuthoredState } from "../types";
import { addLinkedInteractiveStateListener, removeLinkedInteractiveStateListener, showModal } from "@concord-consortium/lara-interactive-api";
import { useLinkedInteractiveId } from "@concord-consortium/question-interactives-helpers/src/hooks/use-linked-interactive-id";
import { DecorateChildren } from "@concord-consortium/text-decorator";
import { renderHTML } from "@concord-consortium/question-interactives-helpers/src/utilities/render-html";
import { useGlossaryDecoration } from "@concord-consortium/question-interactives-helpers/src/hooks/use-glossary-decoration";
import { dataSampleColumnLabel, DataSampleColumnName, dataSampleToTableRow, getSortedColumns, ITectonicExplorerInteractiveState } from "@concord-consortium/tecrock-shared";
import ZoomIn from "../assets/zoom-in.svg";
import { ITectonicExplorerInteractiveState } from "@concord-consortium/tecrock-shared";
import Prompt from "../assets/collect-data-prompt.png";

import css from "./runtime.scss";
Expand All @@ -17,7 +17,7 @@ interface IProps extends IRuntimeQuestionComponentProps<IAuthoredState, ITectoni

export const Runtime: React.FC<IProps> = ({ authoredState, interactiveState, setInteractiveState, report }) => {
const dataSourceInteractive = useLinkedInteractiveId("dataSourceInteractive");
const { dataSamples, dataSampleColumns, planetViewSnapshot, crossSectionSnapshot } = interactiveState || {};
const { dataSamples } = interactiveState || {};

useEffect(() => {
if (!dataSourceInteractive) {
Expand Down Expand Up @@ -45,7 +45,6 @@ export const Runtime: React.FC<IProps> = ({ authoredState, interactiveState, set
};
}, [dataSourceInteractive, setInteractiveState]);

const sortedColumns = dataSampleColumns ? getSortedColumns(dataSampleColumns) : [];
const decorateOptions = useGlossaryDecoration();

const handleExpandSnapshot = useCallback((event: React.MouseEvent<HTMLImageElement>) => {
Expand Down Expand Up @@ -83,52 +82,8 @@ export const Runtime: React.FC<IProps> = ({ authoredState, interactiveState, set
{
// Do not render table when there are no data samples.
dataSamples && dataSamples.length > 0 &&
<div className={css.tableAndSnapshots}>
<div className={css.table}>
<table>
<thead>
<tr>
{
sortedColumns.map((column: DataSampleColumnName) => (
<th key={column} className={css[column]}>{ dataSampleColumnLabel[column] }</th>
))
}
</tr>
</thead>
<tbody>
{
dataSamples.map(sample => dataSampleToTableRow(sample)).map((rockRowData, idx) => (
<tr key={idx}>
{
sortedColumns.map((column: DataSampleColumnName) => (
<td key={column} className={css[column]}>{ rockRowData[column] }</td>
))
}
</tr>
))
}
</tbody>
</table>
</div>
<div className={css.snapshots}>
{
planetViewSnapshot &&
<div className={css.imgContainer}>
<img src={planetViewSnapshot} alt="Planet view snapshot" onClick={handleExpandSnapshot} />
<ZoomIn />
</div>
}
{
crossSectionSnapshot &&
<div className={css.imgContainer}>
<img src={crossSectionSnapshot} alt="Cross-section snapshot" onClick={handleExpandSnapshot} />
<ZoomIn />
</div>
}
</div>
</div>
<Table interactiveState={interactiveState} handleExpandSnapshot={handleExpandSnapshot} />
}

</div>
);
};
3 changes: 3 additions & 0 deletions packages/tecrock-table/src/components/table.inline.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Inline CSS is the same as regular CSS, but it requires a different suffix so that Webpack can distinguish
// between them and use a different loader for each one.
@import "./table.scss";
63 changes: 63 additions & 0 deletions packages/tecrock-table/src/components/table.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.tableAndSnapshots {
display: flex;
flex-direction: row;
margin-top: 10px;

.table {
overflow-x: auto;
margin-right: 10px;
vertical-align: top;

table {
border-collapse: collapse;
table-layout: auto;

td, th {
border: 0.5px solid #979797;
padding: 4px;
text-align: center;

&.temperatureAndPressure {
max-width: 100px;
}
&.type {
max-width: 200px;
}
}

td.notes {
min-width: 200px;
text-align: left;
}
}
}

.snapshots {
// This will result in flex-grow: 1 and ensure that the snapshots will be as wide as possible.
flex: 1;
min-width: 30%;
// Limit max size when snapshots are displayed in the report and user is using a big screen. In this case,
// image could become much bigger than its native size.
max-width: 1000px;

.imgContainer {
position: relative;
width: 100%;
margin-bottom: 5px;
cursor: pointer;

img {
width: 100%;
}

svg {
display: block;
position: absolute;
bottom: 3px;
right: 3px;
height: 30px;
pointer-events: none;
}
}
}
}
Loading

0 comments on commit b5a8171

Please sign in to comment.