diff options
Diffstat (limited to 'src/client/util/reportManager/ReportManager.tsx')
-rw-r--r-- | src/client/util/reportManager/ReportManager.tsx | 382 |
1 files changed, 91 insertions, 291 deletions
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index be46ba0a8..f20c2baaa 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -12,21 +12,17 @@ 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, OrientationType, Type } from 'browndash-components'; -import { BugType, FileData, Priority, ViewState, darkColors, isLightText, lightColors } from './reportManagerUtils'; -import { IssueCard, IssueView, Tag } from './ReportManagerComponents'; +import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; +import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils'; +import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents'; import { StrCast } from '../../../fields/Types'; +import { MdRefresh } from 'react-icons/md'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -// StrCast(Doc.UserDoc().userColor); -// StrCast(Doc.UserDoc().userBackgroundColor); -// StrCast(Doc.UserDoc().userVariantColor); - /** * Class for reporting and viewing Github issues within the app. */ @@ -85,44 +81,28 @@ export class ReportManager extends React.Component<{}> { }); // 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 formData: ReportForm = emptyReportForm; + @action setFormData = action((newData: ReportForm) => { + this.formData = newData; }); - @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[]; - // 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); + 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: {}) { @@ -136,168 +116,53 @@ export class ReportManager extends React.Component<{}> { } /** - * 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', - 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<void> { - if (this.bugTitle === '' || this.bugDescription === '' || this.bugType === '' || this.bugPriority === '') { + if (this.formData.title === '' || this.formData.description === '') { alert('Please fill out all required fields to report an issue.'); return; } + let formattedLinks: string[] = []; this.setSubmitting(true); - - const links = await this.uploadFilesToServer(); - console.log(links); - if (!links) { - // error uploading files to the server - return; + if (this.formData.mediaFiles.length > 0) { + const links = await uploadFilesToServer(this.formData.mediaFiles); + console.log(links); + if (!links) { + return; + } + formattedLinks = links; } - 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], - // }); + const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { + owner: 'brown-dash', + repo: 'Dash-Web', + title: formatTitle(this.formData.title, Doc.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.'); - // return; - // } + // 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.setFormData(emptyReportForm); this.setSubmitting(false); - this.setFetchingIssues(true); - try { - // load in the issues if not already loaded - const issues = (await this.getAllIssues()) as Issue[]; - // 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); + await this.updateIssues(); alert('Successfully submitted issue.'); } /** - * 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<string[] | undefined> => { - 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; + this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: v4(), file }))] }); }; /** @@ -317,7 +182,7 @@ export class ReportManager extends React.Component<{}> { <img height={100} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} /> </div> <div className="close-btn"> - <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} /> + <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} /> </div> </div> ); @@ -331,7 +196,7 @@ export class ReportManager extends React.Component<{}> { </video> </div> <div className="close-btn"> - <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} /> + <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} /> </div> </div> ); @@ -340,7 +205,7 @@ export class ReportManager extends React.Component<{}> { <div key={fileData._id} className="report-audio-wrapper"> <audio src={preview} controls /> <div className="close-btn"> - <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} /> + <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} /> </div> </div> ); @@ -352,94 +217,31 @@ export class ReportManager extends React.Component<{}> { * @returns the component that dispays all issues */ private viewIssuesComponent = () => { - const darkMode = isLightText(StrCast(Doc.UserDoc().userBackgroundColor)); + const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)); const colors = darkMode ? darkColors : lightColors; - const isTagDarkMode = isLightText(StrCast(Doc.UserDoc().userVariantColor)); - const activeTagTextColor = isTagDarkMode ? darkColors.text : lightColors.text; return ( <div className="view-issues" style={{ backgroundColor: StrCast(Doc.UserDoc().userBackgroundColor), color: colors.text }}> <div className="left" style={{ display: this.rightExpanded ? 'none' : 'flex' }}> <div className="report-header"> <h2 style={{ color: colors.text }}>Open Issues</h2> - <Button - type={Type.TERT} - color={StrCast(Doc.UserDoc().userColor)} - text="Report Issue" - onClick={() => { - this.setViewState(ViewState.CREATE); - }} - /> - </div> - <input - className="report-input" - type="text" - placeholder="Filter by query..." - onChange={e => { - this.setQuery(e.target.value); - }} - required - /> - <div className="issues-filters"> - <div className="issues-filter"> - <Tag - text={'All'} - onClick={() => { - this.setPriorityFilter(null); - }} - fontSize="12px" - backgroundColor={this.priorityFilter === null ? StrCast(Doc.UserDoc().userVariantColor) : 'transparent'} - color={this.priorityFilter === null ? activeTagTextColor : colors.textGrey} - border - borderColor={this.priorityFilter === null ? StrCast(Doc.UserDoc().userVariantColor) : colors.border} - /> - {Object.values(Priority).map(p => { - return ( - <Tag - key={p} - text={p} - onClick={() => { - this.setPriorityFilter(p); - }} - fontSize="12px" - backgroundColor={this.priorityFilter === p ? StrCast(Doc.UserDoc().userVariantColor) : 'transparent'} - color={this.priorityFilter === p ? activeTagTextColor : colors.textGrey} - border - borderColor={this.priorityFilter === p ? StrCast(Doc.UserDoc().userVariantColor) : colors.border} - /> - ); - })} - </div> - <div className="issues-filter"> - <Tag - text={'All'} + <div className="header-btns"> + <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="refresh" icon={<MdRefresh size="16px" />} onClick={this.updateIssues} /> + <Button + type={Type.TERT} + color={StrCast(Doc.UserDoc().userVariantColor)} + text="Report Issue" onClick={() => { - this.setBugFilter(null); + this.setViewState(ViewState.CREATE); }} - fontSize="12px" - backgroundColor={this.bugFilter === null ? StrCast(Doc.UserDoc().userVariantColor) : 'transparent'} - color={this.bugFilter === null ? activeTagTextColor : colors.textGrey} - border - borderColor={this.bugFilter === null ? StrCast(Doc.UserDoc().userVariantColor) : colors.border} /> - {Object.values(BugType).map(b => { - return ( - <Tag - key={b} - text={b} - onClick={() => { - this.setBugFilter(b); - }} - fontSize="12px" - backgroundColor={this.bugFilter === b ? StrCast(Doc.UserDoc().userVariantColor) : 'transparent'} - color={this.bugFilter === b ? activeTagTextColor : colors.textGrey} - border - borderColor={this.bugFilter === b ? StrCast(Doc.UserDoc().userVariantColor) : colors.border} - /> - ); - })} </div> </div> + <FormInput value={this.query} placeholder="Filter by query..." onChange={this.setQuery} /> + <div className="issues-filters"> + <Filter items={Object.values(Priority)} activeValue={this.priorityFilter} setActiveValue={p => this.setPriorityFilter(p)} /> + <Filter items={Object.values(BugType)} activeValue={this.bugFilter} setActiveValue={b => this.setBugFilter(b)} /> + </div> <div className="issues"> {this.fetchingIssues ? ( <div style={{ flexGrow: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}> @@ -448,7 +250,7 @@ export class ReportManager extends React.Component<{}> { ) : ( this.shownIssues .filter(issue => issue.title.toLowerCase().includes(this.query)) - .filter(issue => this.passesTagFilter(issue)) + .filter(issue => passesTagFilter(issue, this.priorityFilter, this.bugFilter)) .map(issue => ( <IssueCard key={issue.number} @@ -482,7 +284,7 @@ export class ReportManager extends React.Component<{}> { * @returns the form component for submitting issues */ private reportIssueComponent = () => { - const darkMode = isLightText(StrCast(Doc.UserDoc().userBackgroundColor)); + const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)); const colors = darkMode ? darkColors : lightColors; return ( @@ -502,38 +304,37 @@ export class ReportManager extends React.Component<{}> { </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 /> + <FormInput value={this.formData.title} placeholder="Title..." onChange={val => this.setFormData({ ...this.formData, title: val })} /> </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 /> + <FormTextArea value={this.formData.description} placeholder="Description..." onChange={val => this.setFormData({ ...this.formData, description: val })} /> </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={BugType.BUG}> - Bug - </option> - <option className="report-opt" value={BugType.COSMETIC}> - Poor Design or Cosmetic - </option> - <option className="report-opt" value={BugType.DOCUMENTATION}> - Poor Documentation - </option> - <option className="report-opt" value={BugType.ENHANCEMENT}> - New feature or request - </option> - </select> - <select className="report-select" name="priority" 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> + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Type'} + items={bugDropdownItems} + selectedVal={this.formData.type} + setSelectedVal={val => { + if (typeof val === 'string') this.setFormData({ ...this.formData, type: val as BugType }); + }} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + fillWidth + /> + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Priority'} + items={priorityDropdownItems} + selectedVal={this.formData.priority} + setSelectedVal={val => { + if (typeof val === 'string') this.setFormData({ ...this.formData, priority: val as Priority }); + }} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + fillWidth + /> </div> <Dropzone onDrop={this.onDrop} @@ -545,7 +346,7 @@ export class ReportManager extends React.Component<{}> { 'audio/ogg': ['.ogg'], }}> {({ getRootProps, getInputProps }) => ( - <div {...getRootProps({ className: 'dropzone' })}> + <div {...getRootProps({ className: 'dropzone' })} style={{ borderColor: isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border }}> <input {...getInputProps()} /> <div className="dropzone-instructions"> <AiOutlineUpload size={25} /> @@ -554,12 +355,12 @@ export class ReportManager extends React.Component<{}> { </div> )} </Dropzone> - {this.mediaFiles.length > 0 && <ul className="file-list">{this.mediaFiles.map(file => this.getMediaPreview(file))}</ul>} + {this.formData.mediaFiles.length > 0 && <ul className="file-list">{this.formData.mediaFiles.map(file => this.getMediaPreview(file))}</ul>} {this.submitting ? ( <Button text="Submit" type={Type.TERT} - color={StrCast(Doc.UserDoc().userColor)} + color={StrCast(Doc.UserDoc().userVariantColor)} icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />} iconPlacement="right" onClick={() => { @@ -570,13 +371,12 @@ export class ReportManager extends React.Component<{}> { <Button text="Submit" type={Type.TERT} - color={StrCast(Doc.UserDoc().userColor)} + color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => { this.reportIssue(); }} /> )} - <div style={{ position: 'absolute', top: '4px', right: '4px' }}> <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} /> </div> |