import { Octokit } from '@octokit/core'; import { Button, Dropdown, DropdownType, IconButton, Type } from '@dash/components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { BsArrowsAngleContract, BsArrowsAngleExpand, BsX } from 'react-icons/bs'; import { CgClose } from 'react-icons/cg'; import { HiOutlineArrowLeft } from 'react-icons/hi'; import { MdRefresh } from 'react-icons/md'; import ReactLoading from 'react-loading'; import * as uuid from 'uuid'; import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { MainViewModal } from '../../views/MainViewModal'; import '../SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; import './ReportManager.scss'; import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; import { Issue } from './reportManagerSchema'; import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils'; /** * Class for reporting and viewing Github issues within the app. */ @observer export class ReportManager extends React.Component { // eslint-disable-next-line no-use-before-define 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 formData: ReportForm = emptyReportForm; @action setFormData = action((newData: ReportForm) => { this.formData = newData; }); public close = action(() => { this.isOpen = false; }); public open = action(async () => { this.isOpen = true; if (this.shownIssues.length === 0) { this.updateIssues(); } }); @action updateIssues = action(async () => { this.setFetchingIssues(true); try { const issues = (await getAllIssues(this.octokit)) as Issue[]; this.setShownIssues(issues.filter(issue => issue.state === 'open' && !issue.pull_request)); } catch (err) { console.log(err); } this.setFetchingIssues(false); }); constructor(props: object) { super(props); makeObservable(this); ReportManager.Instance = this; // initializing Github connection this.octokit = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN, }); } /** * Sends a request to Github to report a new issue with the form data. * @returns nothing */ public async reportIssue(): Promise { if (this.formData.title === '' || this.formData.description === '') { alert('Please fill out all required fields to report an issue.'); return; } this.setSubmitting(true); let formattedLinks: string[] = []; if (this.formData.mediaFiles.length > 0) { const links = await uploadFilesToServer(this.formData.mediaFiles); if (links) { formattedLinks = links; } } const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { owner: 'brown-dash', repo: 'Dash-Web', title: formatTitle(this.formData.title, ClientUtils.CurrentUserEmail()), body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`, labels: ['from-dash-app', this.formData.type, this.formData.priority], }); // 201 status means success if (req.status !== 201) { alert('Error creating issue on github.'); } else { await this.updateIssues(); alert('Successfully submitted issue.'); } this.setFormData(emptyReportForm); this.setSubmitting(false); } /** * Handles file upload. * * @param files uploaded files */ private onDrop = (files: File[]) => { this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: uuid.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; const mimeType = file.type; const preview = URL.createObjectURL(file); if (mimeType.startsWith('image/')) { return (
{`Preview
} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
); } if (mimeType.startsWith('video/')) { return (
} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
); } if (mimeType.startsWith('audio/')) { return (
); } return
; }; /** * @returns the component that dispays all issues */ private viewIssuesComponent = () => { const darkMode = isDarkMode(SettingsManager.userBackgroundColor); const colors = darkMode ? darkColors : lightColors; return (

Open Issues

} onClick={this.updateIssues} />
this.setPriorityFilter(p)} /> this.setBugFilter(b)} />
{this.fetchingIssues ? (
) : ( this.shownIssues .filter(issue => issue.title.toLowerCase().includes(this.query)) .filter(issue => passesTagFilter(issue, this.priorityFilter, this.bugFilter)) .map(issue => ( { this.setSelectedIssue(issue); }} /> )) )}
{this.selectedIssue ? :
No issue selected
}
: } onClick={e => { e.stopPropagation(); this.setRightExpanded(!this.rightExpanded); }} /> } onClick={this.close} />
); }; /** * @returns the form component for submitting issues */ private reportIssueComponent = () => { const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)); const colors = darkMode ? darkColors : lightColors; return (
this.setFormData({ ...this.formData, title: val })} />
this.setFormData({ ...this.formData, description: val })} />
{ if (typeof val === 'string') this.setFormData({ ...this.formData, type: val as BugType }); }} dropdownType={DropdownType.SELECT} type={Type.TERT} fillWidth /> { if (typeof val === 'string') this.setFormData({ ...this.formData, priority: val as Priority }); }} dropdownType={DropdownType.SELECT} type={Type.TERT} fillWidth />
{ if (!e.target.files) return; this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...Array.from(e.target.files).map(file => ({ _id: uuid.v4(), file }))] }); }} /> {this.formData.mediaFiles.length > 0 &&
    {this.formData.mediaFiles.map(file => this.getMediaPreview(file))}
} {this.submitting ? (
); }; /** * @returns the component rendered to the modal */ private reportComponent = () => { if (this.viewState === ViewState.VIEW) { return this.viewIssuesComponent(); } return this.reportIssueComponent(); }; render() { return ( ); } }