diff options
Diffstat (limited to 'src/client/util/ReportManager.tsx')
-rw-r--r-- | src/client/util/ReportManager.tsx | 548 |
1 files changed, 0 insertions, 548 deletions
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx deleted file mode 100644 index 125d20876..000000000 --- a/src/client/util/ReportManager.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import { ColorState, SketchPicker } from 'react-color'; -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 { 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 { action, computed, observable, runInAction } from 'mobx'; -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 { Button, IconButton } from '@mui/material'; -import { Oval } from 'react-loader-spinner'; -import Dropzone from 'react-dropzone'; -import ReactMarkdown from 'react-markdown'; -import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import { theme } from '../theme'; -import v4 = require('uuid/v4'); -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - -enum ViewState { - VIEW, - CREATE, -} - -interface FileData { - _id: string; - file: File; -} - -// Format reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" /> - -/** - * Class for reporting and viewing Github issues within the app. - */ -@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 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 fetchingIssues: boolean = false; - @action private setFetchingIssues = (fetching: boolean) => { - this.fetchingIssues = fetching; - }; - - @observable - public shownIssues: Issue[] = []; - @action setShownIssues = action((issues: Issue[]) => { - this.shownIssues = issues; - }); - - @observable selectedIssue: Issue | undefined = undefined; - @action setSelectedIssue = action((issue: Issue | undefined) => { - this.selectedIssue = issue; - }); - - // Form state - - @observable private bugTitle = ''; - @action setBugTitle = action((title: string) => { - this.bugTitle = title; - }); - @observable private bugDescription = ''; - @action setBugDescription = action((description: string) => { - this.bugDescription = description; - }); - @observable private bugType = ''; - @action setBugType = action((type: string) => { - this.bugType = type; - }); - @observable private bugPriority = ''; - @action setBugPriority = action((priortiy: string) => { - this.bugPriority = priortiy; - }); - - @observable private mediaFiles: FileData[] = []; - @action private setMediaFiles = (files: FileData[]) => { - this.mediaFiles = files; - }; - - public close = action(() => (this.isOpen = false)); - public open = action(async () => { - this.isOpen = true; - if (this.shownIssues.length === 0) { - this.setFetchingIssues(true); - try { - // load in the issues if not already loaded - const issues = (await this.getAllIssues()) as Issue[]; - this.setShownIssues(issues.filter(issue => issue.state === 'open' && !issue.pull_request)); - } catch (err) { - console.log(err); - } - this.setFetchingIssues(false); - } - }); - - constructor(props: {}) { - super(props); - ReportManager.Instance = this; - - // initializing Github connection - this.octokit = new Octokit({ - auth: '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', - }); - } - - /** - * Fethches issues from Github. - * @returns array of all issues - */ - public async getAllIssues(): Promise<any[]> { - const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', { - owner: 'brown-dash', - repo: 'Dash-Web', - }); - - // 200 status means success - if (res.status === 200) { - return res.data; - } else { - throw new Error('Error getting issues'); - } - } - - /** - * Sends a request to Github to report a new issue with the form data. - * @returns nothing - */ - public async reportIssue(): Promise<void> { - if (this.bugTitle === '' || this.bugDescription === '' || this.bugType === '' || this.bugPriority === '') { - alert('Please fill out all required fields to report an issue.'); - return; - } - this.setSubmitting(true); - - const links = await this.uploadFilesToServer(); - if (!links) { - // error uploading files to the server - return; - } - 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} ${formattedLinks.length > 0 && `\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.'); - return; - } - - // Reset fields - this.setBugTitle(''); - this.setBugDescription(''); - this.setMediaFiles([]); - this.setBugType(''); - this.setBugPriority(''); - this.setSubmitting(false); - alert('Successfully submitted issue.'); - // this.close(); - } - - /** - * Formats issue title. - * - * @param title title of issue - * @param userEmail email of issue submitter - * @returns formatted title - */ - private formatTitle = (title: string, userEmail: string): string => `${title} - ${userEmail.replace('@brown.edu', '')}`; - - // turns an upload link into a servable link - // ex: - // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png - // -> https://browndash.com/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png - private fileLinktoServerLink = (fileLink: string) => { - const serverUrl = 'https://browndash.com/'; - - const regex = 'public'; - const publicIndex = fileLink.indexOf(regex) + regex.length; - - const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`; - return finalUrl; - }; - - /** - * Gets the server file path. - * - * @param link response from file upload - * @returns server file path - */ - private getServerPath = (link: any): string => { - return link.result.accessPaths.agnostic.server as string; - }; - - /** - * Uploads media files to the server. - * @returns the server paths or undefined on error - */ - private uploadFilesToServer = async (): Promise<string[] | undefined> => { - try { - const links = await Networking.UploadFilesToServer( - this.mediaFiles.map(file => ({ file: file.file })), - true - ); - return (links ?? []).map(this.getServerPath); - } catch (err) { - if (err instanceof Error) { - alert(err.message); - } else { - alert(err); - } - } - }; - - /** - * Handles file upload. - * @param files uploaded files - */ - private onDrop = (files: File[]) => { - this.setMediaFiles([...this.mediaFiles, ...files.map(file => ({ _id: v4(), file }))]); - }; - - /** - * Gets a JSX element to render a media preview - * @param fileData file data - * @returns JSX element of a piece of media (image, video, audio) - */ - private getMediaPreview = (fileData: FileData): JSX.Element => { - const file = fileData.file; - const mimeType = file.type; - const preview = URL.createObjectURL(file); - - if (mimeType.startsWith('image/')) { - return ( - <div key={fileData._id} className="report-media-wrapper"> - <div className="report-media-content"> - <img height={50} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} /> - </div> - <IconButton onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn"> - <BsX color="#ffffff" /> - </IconButton> - </div> - ); - } else if (mimeType.startsWith('video/')) { - return ( - <div key={fileData._id} className="report-media-wrapper"> - <div className="report-media-content"> - <video controls style={{ height: '50px', width: 'auto', display: 'block' }}> - <source src={preview} type="video/mp4" /> - Your browser does not support the video tag. - </video> - </div> - <IconButton onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn"> - <BsX color="#ffffff" /> - </IconButton> - </div> - ); - } else if (mimeType.startsWith('audio/')) { - return ( - <div key={fileData._id} className="report-audio-wrapper"> - <audio src={preview} controls /> - <IconButton onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn"> - <BsX /> - </IconButton> - </div> - ); - } - return <></>; - }; - - /** - * @returns the component that dispays all issues - */ - 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.fetchingIssues ? ( - <div style={{ flexGrow: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}> - <Oval height={50} width={50} color={theme.palette.primary.main} visible={true} ariaLabel="oval-loading" secondaryColor={theme.palette.primary.main + 'b8'} strokeWidth={3} strokeWidthSecondary={3} /> - </div> - ) : ( - 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> - ); - }; - - /** - * @returns the form component for submitting issues - */ - private reportIssueComponent = () => { - return ( - <div className="report-issue"> - <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', '.jpg', '.jpeg', '.gif'], - 'video/*': ['.mp4', '.mpeg', '.webm', '.mov'], - '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 && <ul className="file-list">{this.mediaFiles.map(file => this.getMediaPreview(file))}</ul>} - <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> - ); - }; - - /** - * @returns the component rendered to the modal - */ - private reportComponent = () => { - if (this.viewState === ViewState.VIEW) { - return this.viewIssuesComponent(); - } else { - return this.reportIssueComponent(); - } - }; - - render() { - return ( - <MainViewModal - contents={this.reportComponent()} - isDisplayed={this.isOpen} - interactive={true} - closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: '#ffffff', borderRadius: '8px' }} - /> - ); - } -} - -// Mini components to render issues - -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) => { - const parseBody = (body: string) => { - const imgTagRegex = /<img\b[^>]*\/?>/; - const fileRegex = /https:\/\/browndash\.com\/files/; - const parts = body.split('\n'); - - const modifiedParts = parts.map(part => { - if (imgTagRegex.test(part)) { - return getLinkFromTag(part); - } else if (fileRegex.test(part)) { - return getTagFromUrl(part); - } else { - return part; - } - }); - return modifiedParts.join('\n'); - }; - - const getLinkFromTag = (tag: string) => { - const regex = /src="([^"]+)"/; - let url = ''; - const match = tag.match(regex); - if (match) { - url = match[1]; - } - - return `\n${url}`; - }; - - const getTagFromUrl = (url: string) => { - const imgRegex = /https:\/\/browndash\.com\/files[/\\]images/; - const videoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; - const audioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; - - if (imgRegex.test(url)) { - return `${url}\n<img width="100%" alt="Issue asset" src=${url} />`; - } else if (videoRegex.test(url)) { - return url; - } else if (audioRegex.test(url)) { - return `${url}\n<audio src=${url} controls />`; - } - }; - - 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 ? parseBody(issue.body as string) : ''} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> - </div> - ); -}; |