diff --git a/package.json b/package.json
index 6dde27fbff..8ce85f0ac5 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,11 @@
"@okta/okta-auth-js": "^7.8.0",
"@okta/okta-react": "^6.9.0",
"@okta/okta-signin-widget": "^7.23.0",
+ "@tiptap/extension-mention": "^2.9.1",
+ "@tiptap/pm": "^2.9.1",
+ "@tiptap/react": "^2.9.1",
+ "@tiptap/starter-kit": "^2.9.1",
+ "@tiptap/suggestion": "^2.9.1",
"@toast-ui/react-editor": "^3.2.3",
"@trussworks/react-uswds": "^3.2.0",
"@types/apollo-upload-client": "^17.0.2",
@@ -75,6 +80,7 @@
"redux-devtools-extension": "^2.13.9",
"redux-saga": "^1.3.0",
"redux-saga-routines": "^3.2.2",
+ "tippy.js": "^6.3.7",
"typescript": "^4.9.5",
"uuid": "^8.3.2",
"wildcard-mock-link": "^2.0.3",
@@ -179,7 +185,8 @@
"node-fetch": "2.6.1",
"graphql": "15.8.0",
"@graphql-typed-document-node/core": "3.2.0",
- "@apollo/federation": "0.38.1"
+ "@apollo/federation": "0.38.1",
+ "prosemirror-model": "1.23.0"
},
"comments": {
"on_resolutions": {
diff --git a/src/components/MentionTextArea/MentionList.tsx b/src/components/MentionTextArea/MentionList.tsx
new file mode 100644
index 0000000000..4570d4b57b
--- /dev/null
+++ b/src/components/MentionTextArea/MentionList.tsx
@@ -0,0 +1,111 @@
+/* MentionList renders the TipTap suggestion dropdown in addition to defining
+defining keyboard events */
+
+import React, {
+ forwardRef,
+ useEffect,
+ useImperativeHandle,
+ useState
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Spinner from 'components/Spinner';
+
+import './index.scss';
+
+export const SuggestionLoading = () => {
+ return (
+
+
+
+ );
+};
+
+// Handler dropdown scroll event on keypress
+const scrollIntoView = () => {
+ const selectedElm = document.querySelector('.is-selected');
+ selectedElm?.scrollIntoView({ block: 'nearest' });
+};
+
+const MentionList = forwardRef((props: any, ref) => {
+ const { t } = useTranslation('discussionsMisc');
+
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ // Sets the selected mention within the editor props
+ const selectItem = (index: any) => {
+ const item = props.items[index];
+
+ if (item) {
+ props.command({
+ id: item.username,
+ label: item.displayName,
+ 'tag-type': item.tagType
+ });
+ }
+ };
+
+ const upHandler = () => {
+ setSelectedIndex(
+ (selectedIndex + props.items?.length - 1) % props.items?.length
+ );
+ scrollIntoView();
+ };
+
+ const downHandler = () => {
+ setSelectedIndex((selectedIndex + 1) % props.items?.length);
+ scrollIntoView();
+ };
+
+ const enterHandler = () => {
+ selectItem(selectedIndex);
+ };
+
+ useEffect(() => setSelectedIndex(0), [props.items]);
+
+ useImperativeHandle(ref, () => ({
+ onKeyDown: ({ event }: { event: any }) => {
+ if (event.key === 'ArrowUp' || (event.shiftKey && event.key === 'Tab')) {
+ upHandler();
+ return true;
+ }
+
+ if (
+ event.key === 'ArrowDown' ||
+ (!event.shiftKey && event.key === 'Tab')
+ ) {
+ downHandler();
+ return true;
+ }
+
+ if (event.key === 'Enter') {
+ enterHandler();
+ return true;
+ }
+
+ return false;
+ }
+ }));
+
+ return (
+
+ {props.items?.length ? (
+ props.items?.map((item: any, index: any) => (
+
selectItem(index)}
+ >
+ {item.displayName}
+
+ ))
+ ) : (
+
{t('noResults')}
+ )}
+
+ );
+});
+
+export default MentionList;
diff --git a/src/components/MentionTextArea/index.scss b/src/components/MentionTextArea/index.scss
new file mode 100644
index 0000000000..a3ee045c73
--- /dev/null
+++ b/src/components/MentionTextArea/index.scss
@@ -0,0 +1,104 @@
+@use 'uswds-core' as *;
+
+/* Basic editor styles */
+.tiptap {
+ border: 1px solid black;
+ padding: .5rem;
+
+ p {
+ margin: 0px;
+ }
+
+ &__readonly {
+ margin-bottom: 1rem;
+
+ .ProseMirror {
+ outline: none;
+ border: none;
+ padding: 0;
+ }
+
+ &.notification__content {
+ p {
+ quotes: "“" "”";
+
+ &:first-child::before {
+ content: open-quote;
+ }
+
+ &:last-child::after {
+ content: close-quote;
+ }
+
+ span.react-renderer.node-mention {
+ & ~ .ProseMirror-trailingBreak {
+ display: none;
+ }
+ }
+ }
+ }
+ }
+
+ &__editable {
+ .ProseMirror {
+ min-height: 155px;
+ font-size: 16px;
+ line-height: 22px;
+ }
+ }
+}
+
+[data-tippy-root] {
+ width: 99.7%;
+ margin-left: .1rem !important;
+}
+
+.tippy-box {
+ max-width: none !important;
+}
+
+.mention {
+ color: #005EA2;
+ border: none;
+ background-color: transparent;
+ padding: 0;
+}
+
+.text-base-darker {
+ .mention {
+ color: color($theme-color-base-darker);
+ }
+}
+
+.text-base-darkest {
+ .mention {
+ color: color($theme-color-base-darkest);
+ }
+}
+
+.items {
+ position: relative;
+ background: #FFF;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
+ border: 1px solid color($theme-color-base-lighter);
+ padding: 0;
+ max-height: 300px;
+ overflow: auto;
+}
+
+.item {
+ display: block;
+ border: none;
+ margin: 0;
+ width: 100%;
+ text-align: left;
+ background: transparent;
+ padding-top: 0.65rem;
+ padding-bottom: 0.65rem;
+ border-bottom: 1px solid color($theme-color-base-lighter);
+ min-width: 475px;
+
+ &.is-selected {
+ background-color: #d9e8f6;
+ }
+}
diff --git a/src/components/MentionTextArea/index.tsx b/src/components/MentionTextArea/index.tsx
new file mode 100644
index 0000000000..3ccd15bdb4
--- /dev/null
+++ b/src/components/MentionTextArea/index.tsx
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+// import { useTranslation } from 'react-i18next';
+import Mention from '@tiptap/extension-mention';
+import {
+ EditorContent,
+ NodeViewWrapper,
+ ReactNodeViewRenderer,
+ useEditor
+} from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import classNames from 'classnames';
+
+// import { sortBy } from 'lodash';
+import Alert from 'components/shared/Alert';
+
+import suggestion from './suggestion';
+import { getMentions } from './util';
+
+import './index.scss';
+
+/* The rendered Mention after selected from MentionList
+This component can be any react jsx component, but must be wrapped in
+Attrs of selected mention are accessed through node prop */
+const MentionComponent = ({ node }: { node: any }) => {
+ const { label } = node.attrs;
+
+ // Label may return null if the text was truncated by
+ // In this case don't render the mention, and shift the line up by the height of the non-rendered label
+ if (!label) {
+ return
;
+ }
+
+ return (
+
+ {`@${label}`}
+
+ );
+};
+
+/* Extended TipTap Mention class with additional attributes
+Additionally sets a addNodeView to render custo JSX as mention */
+const CustomMention = Mention.extend({
+ atom: true,
+ selectable: true,
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ 'data-id-db': {
+ default: ''
+ },
+ 'tag-type': {
+ default: ''
+ }
+ };
+ },
+ addNodeView() {
+ return ReactNodeViewRenderer(MentionComponent);
+ }
+});
+
+const MentionTextArea = ({
+ id,
+ setFieldValue,
+ editable,
+ disabled,
+ initialContent,
+ className
+}: {
+ id: string;
+ setFieldValue?: (
+ field: string,
+ value: any,
+ shouldValidate?: boolean | undefined
+ ) => void;
+ editable?: boolean;
+ disabled?: boolean;
+ initialContent?: any;
+ className?: string;
+}) => {
+ // const { t } = useTranslation('');
+
+ const [tagAlert, setTagAlert] = useState(false);
+
+ const fetchUsers = ({ query }: { query: string }) => {
+ return [
+ { username: 'a', displayName: 'Admin lead', tagType: 'other' },
+ {
+ username: 'b',
+ displayName: 'Governance Admin Team',
+ tagType: 'other'
+ },
+ {
+ username: 'c',
+ displayName: 'Governance Review Board (GRB)',
+ tagType: 'other'
+ },
+ {
+ username: 'OSYC',
+ displayName: 'Grant Eliezer',
+ tagType: 'user'
+ },
+ {
+ username: 'MKCK',
+ displayName: 'Forest Brown',
+ tagType: 'user'
+ },
+ {
+ username: 'PJEA',
+ displayName: 'Janae Stokes',
+ tagType: 'user'
+ }
+ ];
+ };
+
+ const editor = useEditor(
+ {
+ editable: editable && !disabled,
+ editorProps: {
+ attributes: {
+ id
+ }
+ },
+ extensions: [
+ StarterKit,
+ CustomMention.configure({
+ HTMLAttributes: {
+ class: 'mention'
+ },
+ suggestion: {
+ ...suggestion,
+ items: fetchUsers
+ }
+ })
+ ],
+ onUpdate: ({ editor: input }: any) => {
+ // Uses the form setter prop (Formik) for mutation input
+ if (setFieldValue) {
+ setFieldValue('content', input?.getHTML());
+ }
+ },
+ // Sets a alert of a mention is selected, and users/teams will be emailed
+ onSelectionUpdate: ({ editor: input }: any) => {
+ setTagAlert(!!getMentions(input?.getJSON()).length);
+ },
+ content: initialContent
+ },
+ [initialContent, disabled]
+ );
+
+ return (
+ <>
+
+
+ {tagAlert && editable && (
+
+ {/* t() */}
+ When you save your discussion, the selected team(s) and individual(s)
+ will be notified via email.
+
+ )}
+ >
+ );
+};
+
+export default MentionTextArea;
diff --git a/src/components/MentionTextArea/suggestion.ts b/src/components/MentionTextArea/suggestion.ts
new file mode 100644
index 0000000000..bd74109404
--- /dev/null
+++ b/src/components/MentionTextArea/suggestion.ts
@@ -0,0 +1,129 @@
+import { ReactRenderer } from '@tiptap/react';
+import tippy from 'tippy.js';
+
+import MentionList, { SuggestionLoading } from './MentionList';
+
+/* Returns the current textarea/RTE editor dimension to append the Mentionslist dropdown
+MentionList should have the same width as this parent clientRect */
+const getClientRect = (props: any) => {
+ const editorID = props.editor.options.editorProps.attributes.id;
+ const elem = document.getElementById(editorID);
+ const rect = elem?.getBoundingClientRect();
+ const mentionRect = props.clientRect();
+
+ return () =>
+ new DOMRect(
+ rect?.left,
+ mentionRect.y,
+ mentionRect.width,
+ mentionRect.height
+ );
+};
+
+const suggestion = {
+ allowSpaces: true,
+ render: () => {
+ let reactRenderer: any;
+ let spinner: any;
+ let popup: any;
+
+ return {
+ // If we had async initial data - load a spinning symbol until onStart gets called
+ // We have hardcoded in memory data for current implementation, doesn't currently get called
+ onBeforeStart: (props: any) => {
+ const editorID = props.editor.options.editorProps.attributes.id;
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ reactRenderer = new ReactRenderer(SuggestionLoading, {
+ props,
+ editor: props.editor
+ });
+
+ spinner = tippy('body', {
+ getReferenceClientRect: getClientRect(props),
+ appendTo: () => document.getElementById(editorID) || document.body,
+ content: reactRenderer.element,
+ showOnCreate: true,
+ interactive: false,
+ trigger: 'manual',
+ placement: 'bottom-start'
+ });
+ },
+
+ // Render any available suggestions when mention trigger is first called - @
+ onStart: (props: any) => {
+ const editorID = props.editor.options.editorProps.attributes.id;
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ spinner[0].hide();
+
+ reactRenderer = new ReactRenderer(MentionList, {
+ props,
+ editor: props.editor
+ });
+
+ popup = tippy('body', {
+ getReferenceClientRect: getClientRect(props),
+ appendTo: () => document.getElementById(editorID) || document.body,
+ content: reactRenderer.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start'
+ });
+ },
+
+ // When async data/suggestions return, hide the spinner and show the updated list
+ onUpdate(props: any) {
+ reactRenderer.updateProps(props);
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup[0].setProps({
+ getReferenceClientRect: getClientRect(props)
+ });
+ spinner[0].setProps({
+ getReferenceClientRect: getClientRect(props)
+ });
+
+ spinner[0].hide();
+
+ popup[0].show();
+ },
+
+ // If a valid character key, render the spinner until onUpdate gets called to rerender updated list
+ onKeyDown(props: any) {
+ if (props.event.key === 'Escape') {
+ popup[0].hide();
+ spinner[0].hide();
+
+ return true;
+ }
+
+ if (props.event.key.length === 1 || props.event.key === 'Backspace') {
+ popup[0].hide();
+
+ spinner[0].show();
+ }
+
+ return reactRenderer.ref?.onKeyDown(props);
+ },
+
+ onExit() {
+ popup[0].destroy();
+ spinner[0].destroy();
+ reactRenderer.destroy();
+ }
+ };
+ }
+};
+
+export default suggestion;
diff --git a/src/components/MentionTextArea/util.tsx b/src/components/MentionTextArea/util.tsx
new file mode 100644
index 0000000000..3fb38d603f
--- /dev/null
+++ b/src/components/MentionTextArea/util.tsx
@@ -0,0 +1,15 @@
+// Possible Util to extract only mentions from content
+// eslint-disable-next-line import/prefer-default-export
+export const getMentions = (data: any) => {
+ const mentions: any = [];
+
+ data?.content?.forEach((para: any) => {
+ para?.content?.forEach((content: any) => {
+ if (content?.type === 'mention') {
+ mentions.push(content?.attrs);
+ }
+ });
+ });
+
+ return mentions;
+};
diff --git a/src/components/Modal/index.scss b/src/components/Modal/index.scss
index 1cd8bcc3a9..29065c585f 100644
--- a/src/components/Modal/index.scss
+++ b/src/components/Modal/index.scss
@@ -2,33 +2,19 @@
@use 'viewports' as *;
.easi-modal {
- &__overlay {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- background-color: rgba(0, 0, 0, 0.6);
- z-index: 400;
- }
-
&__has-title svg {
color: color('primary');
}
&__content {
- position: absolute;
width: 100%;
height: 100%;
max-height: 90vh;
overflow-y: auto;
- background-color: color('white');
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- z-index: 1;
font-size: 1.375em;
- line-height: 1.6em;
@media screen and (min-width: $tablet) {
width: 668px;
diff --git a/src/components/Sidepanel/__snapshots__/index.test.tsx.snap b/src/components/Sidepanel/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000..58a6312b2b
--- /dev/null
+++ b/src/components/Sidepanel/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,72 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Sidepanel > matches snapshot 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/Sidepanel/index.scss b/src/components/Sidepanel/index.scss
new file mode 100644
index 0000000000..db25ab91fe
--- /dev/null
+++ b/src/components/Sidepanel/index.scss
@@ -0,0 +1,31 @@
+@use 'uswds-core' as *;
+@use 'viewports' as *;
+
+.easi-sidepanel {
+ &__content {
+ width: 100%;
+ height: auto;
+ min-height: 100%;
+ right: 0;
+
+ @media screen and (min-width: $desktop) {
+ width: 50%;
+ }
+ }
+
+ &__x-button-container {
+ width: 100%;
+ box-shadow: 0px .25rem .5rem rgba(0, 0, 0, 0.1);
+ padding: 1rem;
+ }
+
+ &__x-button {
+ background: none;
+ border: 0;
+ line-height: 0;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/components/Sidepanel/index.test.tsx b/src/components/Sidepanel/index.test.tsx
new file mode 100644
index 0000000000..824f698fe5
--- /dev/null
+++ b/src/components/Sidepanel/index.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import Modal from 'react-modal';
+import { render, waitFor } from '@testing-library/react';
+
+import Sidepanel from '.';
+
+describe('Sidepanel', () => {
+ beforeAll(() => {
+ Modal.setAppElement(document.body);
+ });
+
+ it('renders without errors', async () => {
+ const { getByText, getByTestId } = render(
+ {}}
+ isOpen
+ modalHeading="modalHeading"
+ testid="testid"
+ >
+ children
+ ,
+ { container: document.body }
+ );
+
+ expect(getByTestId('testid')).toBeInTheDocument();
+ expect(getByText('modalHeading')).toBeInTheDocument();
+ });
+
+ it('matches snapshot', async () => {
+ const { asFragment, getByText, getByTestId } = render(
+ {}}
+ isOpen
+ modalHeading="modalHeading"
+ testid="testid"
+ >
+ children
+ ,
+ { container: document.body }
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('testid')).toBeInTheDocument();
+ expect(getByText('modalHeading')).toBeInTheDocument();
+ });
+
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Sidepanel/index.tsx b/src/components/Sidepanel/index.tsx
new file mode 100644
index 0000000000..cbc4834b11
--- /dev/null
+++ b/src/components/Sidepanel/index.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import ReactModal from 'react-modal';
+import { Icon } from '@trussworks/react-uswds';
+import classNames from 'classnames';
+import noScroll from 'no-scroll';
+
+import './index.scss';
+
+type SidepanelProps = {
+ ariaLabel: string;
+ children: React.ReactNode | React.ReactNodeArray;
+ classname?: string;
+ closeModal: () => void;
+ isOpen: boolean;
+ modalHeading: string;
+ openModal?: () => void;
+ testid: string;
+};
+
+const Sidepanel = ({
+ ariaLabel,
+ children,
+ classname,
+ closeModal,
+ isOpen,
+ modalHeading,
+ openModal,
+ testid
+}: SidepanelProps) => {
+ const handleOpenModal = () => {
+ noScroll.on();
+ if (openModal) {
+ openModal();
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
{modalHeading}
+
+
+ {children}
+
+
+ );
+};
+
+export default Sidepanel;
diff --git a/src/stylesheets/custom.scss b/src/stylesheets/custom.scss
index 3a74b76e2d..cdc4f36ecf 100644
--- a/src/stylesheets/custom.scss
+++ b/src/stylesheets/custom.scss
@@ -263,3 +263,23 @@
.bg-green-5 {
background-color: $green-5;
}
+
+// Modal and Sidepanel shared styles
+.easi-modal, .easi-sidepanel {
+ &__overlay {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: rgba(0, 0, 0, 0.6);
+ z-index: 400;
+ }
+
+ &__content {
+ position: absolute;
+ background-color: #fff;
+ z-index: 1;
+ line-height: 1.6em;
+ }
+}
diff --git a/src/views/DiscussionBoard/DiscussionModalWrapper.tsx b/src/views/DiscussionBoard/DiscussionModalWrapper.tsx
new file mode 100644
index 0000000000..2273b81fe0
--- /dev/null
+++ b/src/views/DiscussionBoard/DiscussionModalWrapper.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Grid, GridContainer } from '@trussworks/react-uswds';
+
+import Sidepanel from 'components/Sidepanel';
+
+type DiscussionModalWrapperProps = {
+ isOpen: boolean;
+ closeModal: () => void;
+ children: React.ReactNode;
+};
+
+const DiscussionModalWrapper = ({
+ isOpen,
+ closeModal,
+ children
+}: DiscussionModalWrapperProps) => {
+ const { t } = useTranslation('discussions');
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default DiscussionModalWrapper;
diff --git a/src/views/DiscussionBoard/index.scss b/src/views/DiscussionBoard/index.scss
new file mode 100644
index 0000000000..a46b6df997
--- /dev/null
+++ b/src/views/DiscussionBoard/index.scss
@@ -0,0 +1,37 @@
+@use 'uswds-core' as *;
+@use 'viewports' as *;
+
+.easi-discussions {
+ &__body {
+ padding: 4rem 2rem 2rem;
+ }
+
+ &__connected {
+ border-left: .25rem solid color('base-lightest');
+ margin-left: .9rem;
+ padding-left: 1.4rem;
+ margin-top: 0.5rem;
+ }
+
+ &__not-connected {
+ padding-left: 2.6rem;
+ }
+
+ &__single-discussion:last-of-type {
+ margin-bottom: -1rem;
+ margin-top: -.5rem;
+ }
+}
+
+.no-button > .usa-accordion__heading > .usa-accordion__button {
+ background-size: 0rem !important;
+}
+
+.discussion-accordion > .usa-accordion__content {
+ padding: 0rem 1rem;
+
+ &:empty {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+}
diff --git a/src/views/DiscussionBoard/index.tsx b/src/views/DiscussionBoard/index.tsx
new file mode 100644
index 0000000000..65e68ae9c6
--- /dev/null
+++ b/src/views/DiscussionBoard/index.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { Button, ButtonGroup } from '@trussworks/react-uswds';
+
+import MentionTextArea from '../../components/MentionTextArea';
+
+import DiscussionModalWrapper from './DiscussionModalWrapper';
+
+type DiscussionBoardProps = {
+ isOpen: boolean;
+ closeModal: () => void;
+ id: string;
+};
+
+function DiscussionBoard({ isOpen, closeModal, id }: DiscussionBoardProps) {
+ return (
+
+ {/* Question */}
+
+ Start a discussion
+
+
+ Have a question or comment that you want to discuss internally with the
+ Governance Admin Team or other Governance Review Board (GRB) members
+ involved in this request? Start a discussion and you’ll be notified when
+ they reply.
+
+
+
+
+
+ closeModal()}>
+ Cancel
+
+
+ Save discussion
+
+
+
+ );
+}
+
+export default DiscussionBoard;
diff --git a/src/views/GovernanceReviewTeam/GRBReview/index.tsx b/src/views/GovernanceReviewTeam/GRBReview/index.tsx
index 5574121ba2..3a327e28e8 100644
--- a/src/views/GovernanceReviewTeam/GRBReview/index.tsx
+++ b/src/views/GovernanceReviewTeam/GRBReview/index.tsx
@@ -35,6 +35,7 @@ import { GRBReviewFormAction } from 'types/grbReview';
import { formatDateLocal } from 'utils/date';
import DocumentsTable from 'views/SystemIntake/Documents/DocumentsTable';
+import DiscussionBoard from '../../DiscussionBoard';
import ITGovAdminContext from '../ITGovAdminContext';
import GRBReviewerForm from './GRBReviewerForm';
@@ -130,6 +131,8 @@ const GRBReview = ({
[history, isForm, id, mutate, showMessage, t]
);
+ const [isDiscussionOpen, setIsDiscussionOpen] = useState(false);
+
return (
<>
{
@@ -363,6 +366,17 @@ const GRBReview = ({
+
+ setIsDiscussionOpen(true)}>
+ View discussion board
+
+
+ setIsDiscussionOpen(false)}
+ id="grb-discussion"
+ />
+