aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/ReportManager.tsx
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-07-10 19:10:34 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-07-10 19:10:34 -0400
commitb1f189ffc7dfe558d5895c8f0cb103ab3e5c17d7 (patch)
tree3b40e13257ca09c11f5dadf0b9ef5811b757e74d /src/client/util/ReportManager.tsx
parentfb939a1d2d59a776d9e3336dfc4a1e028ebc3113 (diff)
filters and better ui
Diffstat (limited to 'src/client/util/ReportManager.tsx')
-rw-r--r--src/client/util/ReportManager.tsx548
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>
- );
-};