aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/ReportManager.tsx
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-07-08 02:58:04 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-07-08 02:58:04 -0400
commitc8eb4ac0242181744d3268b1052582b61dbaf477 (patch)
treeacf49f05715e9933c3d0aa13397220694aebd78b /src/client/util/ReportManager.tsx
parent8e205268443f178a79526cf936fabf787691ec5d (diff)
feat: updated github key and ui
Diffstat (limited to 'src/client/util/ReportManager.tsx')
-rw-r--r--src/client/util/ReportManager.tsx478
1 files changed, 324 insertions, 154 deletions
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
index a08ef9979..3eebb8f15 100644
--- a/src/client/util/ReportManager.tsx
+++ b/src/client/util/ReportManager.tsx
@@ -1,62 +1,82 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
import { ColorState, SketchPicker } from 'react-color';
-import { Doc } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { BoolCast, Cast, StrCast } from '../../fields/Types';
import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
-import { Networking } from '../Network';
-import { MainViewModal } from '../views/MainViewModal';
import { FontIconBox } from '../views/nodes/button/FontIconBox';
import { DragManager } from './DragManager';
import { GroupManager } from './GroupManager';
+import { CheckBox } from '../views/search/CheckBox';
+import { undoBatch } from './UndoManager';
+import * as React from 'react';
import './SettingsManager.scss';
import './ReportManager.scss';
-import { undoBatch } from './UndoManager';
+import { action, computed, observable, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { BsX } from 'react-icons/bs';
+import { BiX } from 'react-icons/bi';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { HiOutlineArrowLeft } from 'react-icons/hi';
+import { Issue } from './reportManagerSchema';
+import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { Networking } from '../Network';
+import { MainViewModal } from '../views/MainViewModal';
import { Octokit } from '@octokit/core';
-import { CheckBox } from '../views/search/CheckBox';
+import { Button, IconButton } from '@mui/material';
+import { Oval } from 'react-loader-spinner';
+import Dropzone from 'react-dropzone';
import ReactLoading from 'react-loading';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
-import { Button, MenuItem, Select, SelectChangeEvent, TextField } from '@mui/material';
-import { FormControl, InputLabel } from '@material-ui/core';
+import { theme } from '../theme';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
+enum ViewState {
+ VIEW,
+ CREATE,
+}
+
@observer
export class ReportManager extends React.Component<{}> {
public static Instance: ReportManager;
@observable private isOpen = false;
+ @observable private query = '';
+ @action private setQuery = (q: string) => {
+ this.query = q;
+ };
+
private octokit: Octokit;
- @observable public issues: any[] = [];
- @action setIssues = action((issues: any[]) => {
- this.issues = issues;
+ @observable viewState: ViewState = ViewState.VIEW;
+ @action private setViewState = (state: ViewState) => {
+ this.viewState = state;
+ };
+ @observable submitting: boolean = false;
+ @action private setSubmitting = (submitting: boolean) => {
+ this.submitting = submitting;
+ };
+
+ @observable
+ public shownIssues: Issue[] = [];
+ @action setShownIssues = action((issues: Issue[]) => {
+ this.shownIssues = issues;
});
- // undefined is the default - null is if the user is making an issue
- @observable public selectedIssue: any = undefined;
- @action setSelectedIssue = action((issue: any) => {
+ @observable selectedIssue: Issue | undefined = undefined;
+ @action setSelectedIssue = action((issue: Issue | undefined) => {
this.selectedIssue = issue;
});
- // only get the open issues
- @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
-
- public updateIssueSearch = action((query: string = '') => {
- if (query === '') {
- this.shownIssues = this.issues.filter(issue => issue.state === 'open');
- return;
- }
- this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
- });
+ @observable private mediaFiles: File[] = [];
+ @action private setMediaFiles = (files: File[]) => {
+ this.mediaFiles = files;
+ };
constructor(props: {}) {
super(props);
@@ -68,17 +88,18 @@ export class ReportManager extends React.Component<{}> {
}
public close = action(() => (this.isOpen = false));
- public open = action(() => {
- if (this.issues.length === 0) {
- // load in the issues if not already loaded
- this.getAllIssues()
- .then(issues => {
- this.setIssues(issues);
- this.updateIssueSearch();
- })
- .catch(err => console.log(err));
- }
+ public open = action(async () => {
this.isOpen = true;
+ if (this.shownIssues.length === 0) {
+ try {
+ // load in the issues if not already loaded
+ const issues = (await this.getAllIssues()) as Issue[];
+ this.setShownIssues(issues.filter(issue => issue.state === 'open'));
+ // this.updateIssueSearch();
+ } catch (err) {
+ console.log(err);
+ }
+ }
});
@observable private bugTitle = '';
@@ -98,6 +119,14 @@ export class ReportManager extends React.Component<{}> {
this.bugPriority = priortiy;
});
+ private showReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ };
+
+ private closeReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ };
+
// private toGithub = false;
// will always be set to true - no alterntive option yet
private toGithub = true;
@@ -133,30 +162,31 @@ export class ReportManager extends React.Component<{}> {
};
public async reportIssue() {
+ console.log(this.bugTitle);
+ console.log('reporting issue');
if (this.bugTitle === '' || this.bugDescription === '' || this.bugType === '' || this.bugPriority === '') {
alert('Please fill out all required fields to report an issue.');
return;
}
+ this.setSubmitting(true);
- if (this.toGithub) {
- const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink);
+ console.log('to github');
+ const links = await this.uploadFilesToServer();
+ const formattedLinks = (links ?? []).map(this.fileLinktoServerLink);
- const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
- owner: 'brown-dash',
- repo: 'Dash-Web',
- title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
- body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
- labels: ['from-dash-app', this.bugType, this.bugPriority],
- });
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
+ body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
+ labels: ['from-dash-app', this.bugType, this.bugPriority],
+ });
- // 201 status means success
- if (req.status !== 201) {
- alert('Error creating issue on github.');
- // on error, don't close the modal
- return;
- }
- } else {
- // if not going to github issues, not sure what to do yet...
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ // on error, don't close the modal
+ return;
}
// if we're down here, then we're good to go. reset the fields.
@@ -166,7 +196,9 @@ export class ReportManager extends React.Component<{}> {
this.setFileLinks([]);
this.setBugType('');
this.setBugPriority('');
- this.close();
+ this.setSubmitting(false);
+ alert('Successfully submitted issue.');
+ // this.close();
}
@observable public fileLinks: any = [];
@@ -191,61 +223,216 @@ export class ReportManager extends React.Component<{}> {
}
};
- @observable private age = '';
+ private uploadFilesToServer = async () => {
+ const links = await Networking.UploadFilesToServer(this.mediaFiles.map(file => ({ file })));
+ console.log('finshed uploading', links.map(this.getServerPath));
+ return (links ?? []).map(this.getServerPath);
+ // this.setFileLinks((links ?? []).map(this.getServerPath));
+ };
- @action private setAge = (e: SelectChangeEvent) => {
- this.age = e.target.value as string;
+ private onDrop = (files: File[]) => {
+ this.setMediaFiles(files);
+ };
+
+ private reportComponent = () => {
+ if (this.viewState === ViewState.VIEW) {
+ return this.viewIssuesComponent();
+ } else {
+ return this.reportIssueComponent();
+ }
+ };
+
+ private viewIssuesComponent = () => {
+ return (
+ <div className="view-issues">
+ <div className="left">
+ <div className="report-header">
+ <h2>Open Issues</h2>
+ <Button
+ variant="contained"
+ onClick={() => {
+ this.setViewState(ViewState.CREATE);
+ }}>
+ report issue
+ </Button>
+ </div>
+ <input
+ className="report-input"
+ type="text"
+ placeholder="Filter..."
+ onChange={e => {
+ this.setQuery(e.target.value);
+ }}
+ required
+ />
+ <div className="issues">
+ {this.shownIssues
+ .filter(issue => issue.title.toLowerCase().includes(this.query))
+ .map(issue => (
+ <IssueCard
+ key={issue.number}
+ issue={issue}
+ onSelect={() => {
+ this.setSelectedIssue(issue);
+ }}
+ />
+ ))}
+ </div>
+ </div>
+ <div className="right">{this.selectedIssue ? <IssueView issue={this.selectedIssue} /> : <div>No issue selected</div>}</div>
+ <IconButton sx={{ position: 'absolute', top: '4px', right: '4px' }} onClick={this.close}>
+ <BiX size="16px" />
+ </IconButton>
+ </div>
+ );
};
private reportIssueComponent = () => {
return (
<div className="report-issue">
- <h2>Report an issue</h2>
- <TextField
- fullWidth
- type="text"
- label="Please leave a title for the bug"
- placeholder="Title..."
- required
- sx={{
- '& .MuiInputBase-input': {
- fontSize: 'inherit',
- },
- }}
- onChange={e => this.setBugTitle(e.target.value)}
- />
- <label>Please leave a description for the bug and how it can be recreated.</label>
- <textarea value={this.bugDescription} placeholder="description" onChange={e => this.setBugDescription(e.target.value as string)} required />
- <FormControl fullWidth>
- <InputLabel id="demo-simple-select-label">Age</InputLabel>
- <Select labelId="demo-simple-select-label" id="demo-simple-select" value={this.age} label="Age" onChange={this.setAge}>
- <MenuItem value={10}>Ten</MenuItem>
- <MenuItem value={20}>Twenty</MenuItem>
- <MenuItem value={30}>Thirty</MenuItem>
- </Select>
- </FormControl>
- <FormControl fullWidth>
- <InputLabel>Bug Type</InputLabel>
- <Select value={this.bugType} label="Bug Type" onChange={e => this.setBugType(e.target.value)}>
- <MenuItem value="bug">Bug</MenuItem>
- <MenuItem value="Poor design or cosmetic">Poor design or cosmetic</MenuItem>
- <MenuItem value="Poor documentation">Poor documentation</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>Bug Priority</InputLabel>
- <Select fullWidth value={this.bugPriority} label="Bug Priority" onChange={e => this.setBugPriority(e.target.value as string)}>
- <MenuItem value="bug">Bug</MenuItem>
- <MenuItem value="Poor design or cosmetic">Poor design or cosmetic</MenuItem>
- <MenuItem value="Poor documentation">Poor documentation</MenuItem>
- </Select>
- </FormControl>
- <Button variant="contained">Submit</Button>
+ <div className="report-header-vertical">
+ <Button
+ variant="text"
+ onClick={() => {
+ this.setViewState(ViewState.VIEW);
+ }}
+ sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+ <HiOutlineArrowLeft color={theme.palette.primary.main} />
+ back to view
+ </Button>
+ <h2>Report an Issue</h2>
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please provide a title for the bug</label>
+ <input className="report-input" value={this.bugTitle} type="text" placeholder="Title..." onChange={e => this.setBugTitle(e.target.value)} required />
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please leave a description for the bug and how it can be recreated</label>
+ <textarea className="report-textarea" value={this.bugDescription} placeholder="Description..." onChange={e => this.setBugDescription(e.target.value)} required />
+ </div>
+ <div className="report-selects">
+ <select className="report-select" name="bugType" onChange={e => (this.bugType = e.target.value)}>
+ <option value="" disabled selected>
+ Type
+ </option>
+ <option className="report-opt" value="priority-low">
+ Bug
+ </option>
+ <option className="report-opt" value="priority-medium">
+ Poor Design or Cosmetic
+ </option>
+ <option className="report-opt" value="priority-high">
+ Poor Documentation
+ </option>
+ </select>
+ <select className="report-select" name="bigPriority" onChange={e => (this.bugPriority = e.target.value)}>
+ <option className="report-opt" value="" disabled selected>
+ Priority
+ </option>
+ <option value="priority-low">Low</option>
+ <option value="priority-medium">Medium</option>
+ <option value="priority-high">High</option>
+ </select>
+ </div>
+ <Dropzone
+ onDrop={this.onDrop}
+ accept={{
+ 'image/png': ['.png'],
+ 'image/jpg': ['.jpg'],
+ 'image/jpeg': ['.jpeg'],
+ 'video/mp4': ['.mp4'],
+ 'video/mpeg': ['.mpeg'],
+ 'video/webm': ['.webm'],
+ 'audio/mpeg': ['.mp3'],
+ 'audio/wav': ['.wav'],
+ 'audio/ogg': ['.ogg'],
+ }}>
+ {({ getRootProps, getInputProps }) => (
+ <div {...getRootProps({ className: 'dropzone' })}>
+ <input {...getInputProps()} />
+ <div className="dropzone-instructions">
+ <AiOutlineUpload size={25} />
+ <p>Drop or select media that shows the bug (optional)</p>
+ </div>
+ </div>
+ )}
+ </Dropzone>
+ {this.mediaFiles.length > 0 && (
+ <div className="files">
+ <ul className="file-list">
+ {this.mediaFiles.map((file, i) => (
+ <li key={file.name} className="file-name">
+ {file.name}
+ <IconButton
+ onClick={() => {
+ this.setMediaFiles(this.mediaFiles.filter(f => f !== file));
+ }}>
+ <BiX />
+ </IconButton>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+
+ <Button
+ variant="contained"
+ sx={{ fontSize: '14px', display: 'flex', alignItems: 'center', gap: '1rem' }}
+ onClick={() => {
+ this.reportIssue();
+ }}>
+ Submit
+ {this.submitting && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff87" strokeWidth={3} strokeWidthSecondary={3} />}
+ </Button>
+ <IconButton sx={{ position: 'absolute', top: '4px', right: '4px' }} onClick={this.close}>
+ <BiX size={'16px'} />
+ </IconButton>
</div>
);
};
- private renderIssue = (issue: any) => {
+ private get reportInterface() {
+ const isReportingIssue = this.selectedIssue === null;
+
+ return (
+ <div className="settings-interface">
+ <div className="issue-list-wrapper">
+ <h3>Current Issues</h3>
+ <input type="text" placeholder="search issues" onChange={e => this.setQuery(e.target.value)}></input>
+ <br />
+ {this.shownIssues.length === 0 ? (
+ <ReactLoading className="loading-center" />
+ ) : (
+ this.shownIssues.map(issue => (
+ <div className="issue-list" key={issue.number} onClick={() => this.setSelectedIssue(issue)}>
+ {issue.title}
+ </div>
+ ))
+ )}
+ {/* <div className="settings-user">
+ <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
+ </div> */}
+ </div>
+
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+
+ <div className="issue-content" style={{ paddingTop: this.selectedIssue === undefined ? '50px' : 'inherit' }}>
+ {this.selectedIssue === undefined ? 'no issue selected' : this.renderIssue(this.selectedIssue)}
+ </div>
+
+ <div className="report-issue-fab">
+ <span className="report-disclaimer" hidden={!isReportingIssue}>
+ Note: issue reporting is not anonymous.
+ </span>
+ <button onClick={() => (isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen())}>{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
+ </div>
+ </div>
+ );
+ }
+
+ private renderIssue = (issue: Issue) => {
const isReportingIssue = issue === null;
return isReportingIssue ? (
@@ -275,7 +462,7 @@ export class ReportManager extends React.Component<{}> {
<option value="priority-high">Poor Documentation</option>
</select>
- <select name="bigPriority" onChange={e => (this.bugPriority = e.target.value)}>
+ <select name="priority" onChange={e => (this.bugPriority = e.target.value)}>
<option value="" disabled selected>
Priority
</option>
@@ -304,71 +491,54 @@ export class ReportManager extends React.Component<{}> {
</a>
</h5>
<div className="issue-title">{issue.title}</div>
- <ReactMarkdown children={issue.body} className="issue-body" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ <ReactMarkdown children={issue.body as string} className="issue-body" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
</div>
);
};
- private showReportIssueScreen = () => {
- this.setSelectedIssue(null);
- };
-
- private closeReportIssueScreen = () => {
- this.setSelectedIssue(undefined);
- };
-
- private get reportInterface() {
- const isReportingIssue = this.selectedIssue === null;
-
- return (
- <div className="settings-interface">
- <div className="issue-list-wrapper">
- <h3>Current Issues</h3>
- <input type="text" placeholder="search issues" onChange={e => this.updateIssueSearch(e.target.value)}></input>
- <br />
- {this.issues.length === 0 ? (
- <ReactLoading className="loading-center" />
- ) : (
- this.shownIssues.map(issue => (
- <div className="issue-list" key={issue.number} onClick={() => this.setSelectedIssue(issue)}>
- {issue.title}
- </div>
- ))
- )}
-
- {/* <div className="settings-user">
- <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
- </div> */}
- </div>
-
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
- </div>
-
- <div className="issue-content" style={{ paddingTop: this.selectedIssue === undefined ? '50px' : 'inherit' }}>
- {this.selectedIssue === undefined ? 'no issue selected' : this.renderIssue(this.selectedIssue)}
- </div>
-
- <div className="report-issue-fab">
- <span className="report-disclaimer" hidden={!isReportingIssue}>
- Note: issue reporting is not anonymous.
- </span>
- <button onClick={() => (isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen())}>{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
- </div>
- </div>
- );
- }
-
render() {
return (
<MainViewModal
// contents={this.reportInterface}
- contents={this.reportIssueComponent()}
+ contents={this.reportComponent()}
isDisplayed={this.isOpen}
interactive={true}
closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ dialogueBoxStyle={{ width: 'auto', minWidth: '400px', height: '85vh', maxHeight: '90vh', background: '#ffffff', borderRadius: '8px' }}
/>
);
}
}
+
+interface IssueCardProps {
+ issue: Issue;
+ onSelect: () => void;
+}
+const IssueCard = ({ issue, onSelect }: IssueCardProps) => {
+ return (
+ <div className="issue" onClick={onSelect}>
+ <label className="issue-label">#{issue.number}</label>
+ <h3 className="issue-title">{issue.title}</h3>
+ </div>
+ );
+};
+
+interface IssueViewProps {
+ issue: Issue;
+}
+
+const IssueView = ({ issue }: IssueViewProps) => {
+ return (
+ <div className="issue-view">
+ <span className="issue-label">
+ Issue{' '}
+ <a className="issue-link" href={issue.html_url} target="_blank">
+ #{issue.number}
+ </a>
+ </span>
+ <h2 className="issue-title">{issue.title}</h2>
+ <ReactMarkdown children={issue.body as string} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ {/* <p className="issue-content">{issue.body}</p> */}
+ </div>
+ );
+};