import * as React from 'react'; import '.././SettingsManager.scss'; import './ReportManager.scss'; import { action, observable } from 'mobx'; import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs'; import { CgClose } from 'react-icons/cg'; 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, Tooltip } from '@mui/material'; import Dropzone from 'react-dropzone'; import { theme } from '../../theme'; import ReactLoading from 'react-loading'; import v4 = require('uuid/v4'); import { BugType, FileData, Priority, ViewState, inactiveBorderColor, inactiveColor } from './reportManagerUtils'; import { IssueCard, IssueView, Tag } from './ReportManagerComponents'; // import { IconButton } from 'browndash-components'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; /** * 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 public priorityFilter: Priority | null = null; @action setPriorityFilter = action((priority: Priority | null) => { this.priorityFilter = priority; }); @observable public bugFilter: BugType | null = null; @action setBugFilter = action((bug: BugType | null) => { this.bugFilter = bug; }); @observable selectedIssue: Issue | undefined = undefined; @action setSelectedIssue = action((issue: Issue | undefined) => { this.selectedIssue = issue; }); @observable rightExpanded: boolean = false; @action setRightExpanded = action((expanded: boolean) => { this.rightExpanded = expanded; }); // 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[]; console.log(issues); // filtering to include only open issues and exclude pull requests, maybe add a separate tab for pr's? 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: process.env.GITHUB_ACCESS_TOKEN, }); } /** * Fethches issues from Github. * @returns array of all issues */ public async getAllIssues(): Promise { const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', { owner: 'brown-dash', repo: 'Dash-Web', per_page: 80, }); // 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 { 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(); console.log(links); 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 -> server 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 => { try { // need to always upload to browndash 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 }))]); }; /** * Returns when the issue passes the current filters. * * @param issue issue to check * @returns boolean indicating whether the issue passes the current filters */ private passesTagFilter = (issue: Issue) => { let passesPriority = true; let passesBug = true; if (this.priorityFilter) { passesPriority = issue.labels.some(label => { if (typeof label === 'string') { return label === this.priorityFilter; } else { return label.name === this.priorityFilter; } }); } if (this.bugFilter) { passesBug = issue.labels.some(label => { if (typeof label === 'string') { return label === this.bugFilter; } else { return label.name === this.bugFilter; } }); } return passesPriority && passesBug; }; /** * 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 (
{`Preview
this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn">
); } else if (mimeType.startsWith('video/')) { return (
this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn">
); } else if (mimeType.startsWith('audio/')) { return (
); } return <>; }; /** * @returns the component that dispays all issues */ private viewIssuesComponent = () => { return (

Open Issues

{ this.setQuery(e.target.value); }} required />
{ this.setPriorityFilter(null); }} fontSize="12px" backgroundColor={this.priorityFilter === null ? theme.palette.primary.main : '#ffffff'} color={this.priorityFilter === null ? '#ffffff' : inactiveColor} border borderColor={this.priorityFilter === null ? theme.palette.primary.main : inactiveBorderColor} /> {Object.values(Priority).map(p => { return ( { this.setPriorityFilter(p); }} fontSize="12px" backgroundColor={this.priorityFilter === p ? theme.palette.primary.main : '#ffffff'} color={this.priorityFilter === p ? '#ffffff' : inactiveColor} border borderColor={this.priorityFilter === p ? theme.palette.primary.main : inactiveBorderColor} /> ); })}
{ this.setBugFilter(null); }} fontSize="12px" backgroundColor={this.bugFilter === null ? theme.palette.primary.main : '#ffffff'} color={this.bugFilter === null ? '#ffffff' : inactiveColor} border borderColor={this.bugFilter === null ? theme.palette.primary.main : inactiveBorderColor} /> {Object.values(BugType).map(b => { return ( { this.setBugFilter(b); }} fontSize="12px" backgroundColor={this.bugFilter === b ? theme.palette.primary.main : '#ffffff'} color={this.bugFilter === b ? '#ffffff' : inactiveColor} border borderColor={this.bugFilter === b ? theme.palette.primary.main : inactiveBorderColor} /> ); })}
{this.fetchingIssues ? (
) : ( this.shownIssues .filter(issue => issue.title.toLowerCase().includes(this.query)) .filter(issue => this.passesTagFilter(issue)) .map(issue => ( { this.setSelectedIssue(issue); }} /> )) )}
{this.selectedIssue ? :
No issue selected
}
{ e.stopPropagation(); this.setRightExpanded(!this.rightExpanded); }}> {this.rightExpanded ? : }
); }; /** * @returns the form component for submitting issues */ private reportIssueComponent = () => { return (

Report an Issue

this.setBugTitle(e.target.value)} required />