Skip to content

Commit

Permalink
fetch and render help content (#87)
Browse files Browse the repository at this point in the history
Implement a `Help` component that fetches and renders help content from the server. 

* rename HelpContent to TextualHelp to clarify the type of content it is responsible for rendering

* add styling for handling help content overflow

* update documentation
  • Loading branch information
dpgraham4401 authored Mar 26, 2024
1 parent 107f43a commit d438e0f
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 78 deletions.
26 changes: 26 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
3. [Nomenclature](#nomenclature)
4. [Implementation Notes](#implementation-notes)
5. [Configuration](#configuration)

- [Node Properties](#shared-node-properties)
- [DefaultNode](#defaultnode)
- [BoolNode](#boolnode)
- [Help Content](#help-content)

6. [Deployment](#deployment)
7. [Future Work](#future-work)

Expand Down Expand Up @@ -123,6 +129,9 @@ requires a different configuration.
- **data.help**: A boolean value that determines if the node has help content. If true, the node will display a help
icon
that will display help content when clicked.
- **data.help**: A boolean value that determines if the node has help content. If true, the decision tree expects to be
able to find a JSON file in `public/help/` with the same name as the node id. A question mark icon will be displayed
on node if true. See [Help Content](#help-content) for more information.

### DefaultNode

Expand Down Expand Up @@ -159,6 +168,21 @@ requires a different configuration.
}
```

### Help Content

Nodes can optionally have help content, which can be useful for providing additional information to the user when the
node merits further explanation.

Currently, we only support text based help content, stored in a JSON encoded file following the below schema.
See [future work](#future-work).

```json
{
"type": "text",
"content": "Welcome to the Manifest Game!\n\n This decision tree will help you..."
}
```

## Deployment

The development server can be started using the npm run dev command. This will start a server on port 3000 (by default).
Expand All @@ -183,3 +207,5 @@ docker compose up
2. A new custom node type for multiple choice questions (e.g., what type of site are you? A generator, a TSDF, or a
transporter).
3. Allow EPA to create multiple tree for users.
4. Markup help text. Currently, we only support text based help content, stored in a JSON encoded file. Being able to
provide more complex help content could make the tool more useful as it would allow linking to other resources.
17 changes: 1 addition & 16 deletions public/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,7 @@
"id": "goRegister",
"data": {
"label": "Time to Register in RCRAInfo!",
"children": [
"test1",
"test2"
]
}
},
{
"id": "test1",
"data": {
"label": "Test 1"
}
},
{
"id": "test2",
"data": {
"label": "Test 2"
"children": []
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion public/help/root.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"type": "text",
"content": "Hello, World!"
"content": "Welcome to the Manifest Game!\n\n This interactive decision tree will help guide you through common questions and scenarios when using the Environmental Protection Agency's (EPA) e-Manifest system."
}
48 changes: 48 additions & 0 deletions src/components/Help/Help.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import '@testing-library/jest-dom';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { Help } from 'components/Help/Help';
import { delay, http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import useTreeStore from 'store';
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';

const handlers = [
http.get('/help/:nodeId.json', async (info) => {
const nodeId = info.params.nodeId;
await delay(500);

return HttpResponse.json({
type: 'text',
content: `Help Text ${nodeId}`,
});
}),
];

const server = setupServer(...handlers);

afterEach(() => {
cleanup();
server.resetHandlers(...handlers);
});
beforeAll(() => server.listen());
afterAll(() => server.close());

describe('Help', () => {
test('renders error message when help content ID is undefined', () => {
render(<Help />);
expect(screen.getByText(/problem/i)).toBeInTheDocument();
});
test('renders loader while fetching content', () => {
const helpContentId = 'root';
useTreeStore.setState({ isOpen: true, helpContentId });
render(<Help />);
expect(screen.getByTestId(/helpSpinner/i)).toBeInTheDocument();
});
test('renders help content after fetch', async () => {
const helpContentId = 'root';
useTreeStore.setState({ isOpen: true, helpContentId });
render(<Help />);
await waitFor(() => expect(screen.queryByTestId(/helpSpinner/i)).not.toBeInTheDocument());
expect(screen.getByText(/Help Text root/i)).toBeInTheDocument();
});
});
28 changes: 28 additions & 0 deletions src/components/Help/Help.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TextualHelp } from 'components/Help/HelpContent/TextualHelp';
import { Spinner } from 'components/Spinner/Spinner';
import { useFetchHelp } from 'hooks';
import { useHelp } from 'hooks/useHelp/useHelp';
import React from 'react';

/**
* Responsible for retrieving, and displaying information to help users made decisions
* @constructor
*/
export const Help = () => {
const { helpContentId } = useHelp();
const { help, error, isLoading } = useFetchHelp(helpContentId);

if (helpContentId === undefined || error) {
return <p>There was a problem fetching help.</p>;
}

if (isLoading) {
return <Spinner testId={'helpSpinner'} />;
}

return (
<>
<TextualHelp help={help} />
</>
);
};
29 changes: 0 additions & 29 deletions src/components/Help/HelpContent/HelpContent.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import '@testing-library/jest-dom';
import { cleanup, render, screen } from '@testing-library/react';
import { TextHelp, TextualHelp } from 'components/Help/HelpContent/TextualHelp';
import { afterEach, describe, expect, test } from 'vitest';
import { HelpContent, TextHelp } from './HelpContent';

afterEach(() => cleanup());

describe('HelpContent', () => {
describe('TextualHelp', () => {
test('renders', () => {
render(<HelpContent />);
render(<TextualHelp />);
expect(screen.getByTestId(/help-content/i)).toBeInTheDocument();
});
test('returns a friendly message if help prop is empty', () => {
render(<HelpContent />);
render(<TextualHelp />);
expect(screen.getByText(/unavailable/i, { exact: false })).toBeInTheDocument();
});
test('accepts an TextHelp object and displays the data as text', () => {
const help: TextHelp = {
type: 'text',
data: 'This is a help message',
content: 'This is a help message',
};
render(<HelpContent help={help} />);
expect(screen.getByText(help.data)).toBeInTheDocument();
render(<TextualHelp help={help} />);
expect(screen.getByText(help.content)).toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions src/components/Help/HelpContent/TextualHelp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

export interface TextHelp {
type: 'text';
content: string;
}

interface HelpContentProps {
help?: TextHelp;
}

/**
* Renders the textual help content
* @constructor
*/
export const TextualHelp = ({ help }: HelpContentProps) => {
return (
<div
data-testid={'help-content'}
style={{
whiteSpace: 'pre-line',
fontSize: '1.2rem',
maxHeight: '100%',
}}
>
{help ? <p>{help.content}</p> : <p> Help is unavailable for this node.</p>}
</div>
);
};
6 changes: 3 additions & 3 deletions src/components/OffCanvas/OffCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Help } from 'components/Help/Help';
import styles from 'components/OffCanvas/offcanvas.module.css';
import React, { useCallback, useEffect } from 'react';

Expand All @@ -11,7 +12,7 @@ interface OffCanvasProps {
* Sidebar for displaying content and help
* @constructor
*/
export const OffCanvas = ({ title = 'Help', isOpen, handleClose }: OffCanvasProps) => {
export const OffCanvas = ({ title = 'More Information', isOpen, handleClose }: OffCanvasProps) => {
/** handle when user clicks outside the off canvas component*/
const onClickOutside = useCallback(() => {
if (isOpen) {
Expand Down Expand Up @@ -65,8 +66,7 @@ export const OffCanvas = ({ title = 'Help', isOpen, handleClose }: OffCanvasProp
/>
</div>
<div className={styles.content}>
Some text as placeholder. In real life you can have the elements you have chosen. Like,
text, images, lists, etc.
<Help />
</div>
</div>
{/* backdrop while open*/}
Expand Down
3 changes: 3 additions & 0 deletions src/components/OffCanvas/offcanvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ body {
.content {
font-family: sans-serif;
padding: 0 2rem;
overflow: hidden scroll;
height: 100%;
scrollbar-color: #333333 transparent;
}

.offcanvas {
Expand Down
8 changes: 6 additions & 2 deletions src/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import styles from 'components/Spinner/spinner.module.css';

interface SpinnerProps {
testId?: string;
}

/**
* Spinner - a loading spinner
* https://loading.io/css/
* @constructor
*/
export const Spinner = () => {
export const Spinner = ({ testId }: SpinnerProps) => {
return (
<div className={styles.center} data-testid="spinner">
<div className={styles.center} data-testid={testId ?? 'spinner'}>
<div className={styles.spinnerRing}>
<div></div>
<div></div>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { useDecisionTree } from './useDecisionTree/useDecisionTree';
export { useFetchConfig } from './useFetchConfig/useFetchConfig';
export { useFetchHelp } from './useFetchHelp/useFetchHelp';
export { useTreeDirection } from './useTreeDirection/useTreeDirection';
export { useTreeViewport } from './useTreeViewport/useTreeViewport';
40 changes: 20 additions & 20 deletions src/hooks/useFetchHelp/useFetchHelp.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import { TextHelp } from 'components/Help/HelpContent/TextualHelp';
import { useEffect, useState } from 'react';

/** Configuration for an individual node, part of the larger config*/
export interface HelpConfig {
type: string;
content: string;
}
export type HelpConfig = TextHelp;

/** Hook to fetch the help text for a given node from the server. */
export const useFetchHelp = (nodeId: string) => {
export const useFetchHelp = (nodeId?: string) => {
const [help, setHelp] = useState<HelpConfig | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<unknown | undefined>();

useEffect(() => {
setIsLoading(true);
fetch(`/help/${nodeId}.json`)
.then((response) => {
if (!response.ok) {
throw new Error('There was a problem fetching help.');
}
return response.json();
})
.then((data) => {
setHelp(data);
setIsLoading(false);
})
.catch((e) => {
setError(e);
setIsLoading(false);
});
if (nodeId) {
fetch(`/help/${nodeId}.json`)
.then((response) => {
if (!response.ok) {
throw new Error('There was a problem fetching help.');
}
return response.json();
})
.then((data) => {
setHelp(data);
setIsLoading(false);
})
.catch((e) => {
setError(e);
setIsLoading(false);
});
}
}, [nodeId]);

return {
Expand Down

0 comments on commit d438e0f

Please sign in to comment.