diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-08 02:58:04 -0400 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-08 02:58:04 -0400 |
commit | c8eb4ac0242181744d3268b1052582b61dbaf477 (patch) | |
tree | acf49f05715e9933c3d0aa13397220694aebd78b /src/client/util/ReportManager.tsx | |
parent | 8e205268443f178a79526cf936fabf787691ec5d (diff) |
feat: updated github key and ui
Diffstat (limited to 'src/client/util/ReportManager.tsx')
-rw-r--r-- | src/client/util/ReportManager.tsx | 478 |
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> + ); +}; |