aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-07-09 00:59:10 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-07-09 00:59:10 -0400
commitfb939a1d2d59a776d9e3336dfc4a1e028ebc3113 (patch)
tree2ef9d0ac0419d5d98644dc6450df215569f729d9 /src
parentc8eb4ac0242181744d3268b1052582b61dbaf477 (diff)
Almost done
Diffstat (limited to 'src')
-rw-r--r--src/client/Network.ts16
-rw-r--r--src/client/util/ReportManager.scss98
-rw-r--r--src/client/util/ReportManager.tsx468
3 files changed, 324 insertions, 258 deletions
diff --git a/src/client/Network.ts b/src/client/Network.ts
index d606b9854..eb827e0c8 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -5,7 +5,7 @@ import { Upload } from '../server/SharedMediaTypes';
/**
* Networking is repsonsible for connecting the client to the server. Networking
* mainly provides methods that the client can use to begin the process of
- * interacting with the server, such as fetching or uploading files.
+ * interacting with the server, such as fetching or uploading files.
*/
export namespace Networking {
export async function FetchFromServer(relativeRoute: string) {
@@ -25,9 +25,9 @@ export namespace Networking {
/**
* FileGuidPair attaches a guid to a file that is being uploaded,
* allowing the client to track the upload progress.
- *
+ *
* When files are dragged to the canvas, the overWriteDoc's ID is
- * used as the guid. Otherwise, a new guid is generated.
+ * used as the guid. Otherwise, a new guid is generated.
*/
export interface FileGuidPair {
file: File;
@@ -40,7 +40,7 @@ export namespace Networking {
* @param fileguidpairs the files and corresponding guids to be uploaded to the server
* @returns the response as a json from the server
*/
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[]): Promise<Upload.FileResponse<T>[]> {
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[], browndash?: boolean): Promise<Upload.FileResponse<T>[]> {
const formData = new FormData();
if (Array.isArray(fileguidpairs)) {
if (!fileguidpairs.length) {
@@ -57,17 +57,19 @@ export namespace Networking {
])
);
}
- // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid.
+ // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid.
fileguidpairs.forEach(fileguidpair => formData.append(fileguidpair.guid ?? Utils.GenerateGuid(), fileguidpair.file));
} else {
- // Handle the case where fileguidpairs is a single file.
+ // Handle the case where fileguidpairs is a single file.
formData.append(fileguidpairs.guid ?? Utils.GenerateGuid(), fileguidpairs.file);
}
const parameters = {
method: 'POST',
body: formData,
};
- const response = await fetch('/uploadFormData', parameters);
+
+ const endpoint = '/uploadFormData';
+ const response = await fetch(endpoint, parameters);
return response.json();
}
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
index 4ff86fd9c..b3bd998e6 100644
--- a/src/client/util/ReportManager.scss
+++ b/src/client/util/ReportManager.scss
@@ -31,7 +31,8 @@
// Report
.report-issue {
- width: 45vw;
+ width: 450px;
+ min-width: 300px;
padding: 16px;
padding-top: 32px;
display: flex;
@@ -61,10 +62,11 @@
.report-selects {
display: flex;
+ flex-direction: column;
+ align-items: stretch;
gap: 16px;
.report-select {
- flex: 1;
padding: 8px;
border-color: #c6c6c6;
@@ -95,7 +97,7 @@
.view-issues {
width: 65vw;
- min-width: 600px;
+ min-width: 500px;
display: flex;
gap: 16px;
height: 100%;
@@ -134,6 +136,12 @@
}
}
+// video
+
+.default-video::-webkit-media-controls {
+ display: block !important;
+}
+
// Issue
.issue {
@@ -153,7 +161,6 @@
cursor: pointer;
font-size: 14px;
font-weight: 400;
- letter-spacing: 1px;
color: #7f7f7f;
}
@@ -200,26 +207,24 @@
}
}
-.files {
+.file-list {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
font-size: 14px;
color: #7f7f7f;
+ width: 100%;
+ overflow-x: auto;
+ list-style-type: none;
+ display: flex;
+ gap: 16px;
- .file-list {
- width: 100%;
- list-style-type: none;
+ .file-name {
+ padding: 8px 12px;
display: flex;
- overflow-x: auto;
+ align-items: center;
gap: 16px;
- margin: 0;
- padding: 0;
-
- .file-name {
- padding: 8px 12px;
- display: flex;
- align-items: center;
- gap: 16px;
- white-space: nowrap;
- }
+ white-space: nowrap;
}
}
@@ -257,6 +262,61 @@
}
}
+// Media previews
+
+.report-media-wrapper {
+ position: relative;
+
+ .close-btn {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ opacity: 0;
+ }
+
+ .report-media-content {
+ position: relative;
+ display: inline block;
+ cursor: pointer;
+ }
+
+ .report-media-content::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5); /* Adjust the opacity as desired */
+ opacity: 0;
+ transition: opacity 0.3s ease; /* Transition for smooth effect */
+ }
+
+ &:hover {
+ .report-media-content::after {
+ opacity: 1;
+ }
+
+ .close-btn {
+ opacity: 1;
+ }
+ }
+}
+
+.report-audio-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+@media (max-width: 1100px) {
+ .report-header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 2rem;
+ }
+}
+
// Old code
// <---------------------------------------------------------------------------->
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
index 3eebb8f15..125d20876 100644
--- a/src/client/util/ReportManager.tsx
+++ b/src/client/util/ReportManager.tsx
@@ -13,7 +13,6 @@ import * as React from 'react';
import './SettingsManager.scss';
import './ReportManager.scss';
import { action, computed, observable, runInAction } from 'mobx';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { BsX } from 'react-icons/bs';
import { BiX } from 'react-icons/bi';
import { AiOutlineUpload } from 'react-icons/ai';
@@ -27,11 +26,11 @@ import { Octokit } from '@octokit/core';
import { Button, IconButton } from '@mui/material';
import { Oval } from 'react-loader-spinner';
import Dropzone from 'react-dropzone';
-import ReactLoading from 'react-loading';
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;
@@ -41,6 +40,16 @@ enum ViewState {
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;
@@ -62,6 +71,11 @@ export class ReportManager extends React.Component<{}> {
this.submitting = submitting;
};
+ @observable fetchingIssues: boolean = false;
+ @action private setFetchingIssues = (fetching: boolean) => {
+ this.fetchingIssues = fetching;
+ };
+
@observable
public shownIssues: Issue[] = [];
@action setShownIssues = action((issues: Issue[]) => {
@@ -73,34 +87,7 @@ export class ReportManager extends React.Component<{}> {
this.selectedIssue = issue;
});
- @observable private mediaFiles: File[] = [];
- @action private setMediaFiles = (files: File[]) => {
- this.mediaFiles = files;
- };
-
- constructor(props: {}) {
- super(props);
- ReportManager.Instance = this;
-
- this.octokit = new Octokit({
- auth: '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',
- });
- }
-
- public close = action(() => (this.isOpen = false));
- public open = action(async () => {
- this.isOpen = true;
- if (this.shownIssues.length === 0) {
- try {
- // load in the issues if not already loaded
- const issues = (await this.getAllIssues()) as Issue[];
- this.setShownIssues(issues.filter(issue => issue.state === 'open'));
- // this.updateIssueSearch();
- } catch (err) {
- console.log(err);
- }
- }
- });
+ // Form state
@observable private bugTitle = '';
@action setBugTitle = action((title: string) => {
@@ -119,20 +106,41 @@ export class ReportManager extends React.Component<{}> {
this.bugPriority = priortiy;
});
- private showReportIssueScreen = () => {
- this.setSelectedIssue(undefined);
+ @observable private mediaFiles: FileData[] = [];
+ @action private setMediaFiles = (files: FileData[]) => {
+ this.mediaFiles = files;
};
- private closeReportIssueScreen = () => {
- this.setSelectedIssue(undefined);
- };
+ 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);
+ }
+ });
- // private toGithub = false;
- // will always be set to true - no alterntive option yet
- private toGithub = true;
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
- private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+ // 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',
@@ -147,53 +155,42 @@ export class ReportManager extends React.Component<{}> {
}
}
- // turns an upload link into a servable link
- // ex:
- // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
- // -> http://localhost:1050/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;
- };
-
- public async reportIssue() {
- console.log(this.bugTitle);
- console.log('reporting issue');
+ /**
+ * 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);
- console.log('to github');
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} \n\nfiles:\n${formattedLinks.join('\n')}`,
+ 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.');
- // on error, don't close the modal
return;
}
- // if we're down here, then we're good to go. reset the fields.
+ // Reset fields
this.setBugTitle('');
this.setBugDescription('');
- // this.toGithub = false;
- this.setFileLinks([]);
+ this.setMediaFiles([]);
this.setBugType('');
this.setBugPriority('');
this.setSubmitting(false);
@@ -201,47 +198,118 @@ export class ReportManager extends React.Component<{}> {
// this.close();
}
- @observable public fileLinks: any = [];
- @action setFileLinks = action((links: any) => {
- this.fileLinks = links;
- });
+ /**
+ * 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', '')}`;
- private getServerPath = (link: any) => {
- return link.result.accessPaths.agnostic.server;
+ // 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;
};
- private uploadFiles = (input: any) => {
- // keep null while uploading
- this.setFileLinks(null);
- // upload the files to the server
- if (input.files && input.files.length !== 0) {
- const fileArray: File[] = Array.from(input.files);
- Networking.UploadFilesToServer(fileArray.map(file => ({ file }))).then(links => {
- console.log('finshed uploading', links.map(this.getServerPath));
- this.setFileLinks((links ?? []).map(this.getServerPath));
- });
- }
+ /**
+ * 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;
};
- private uploadFilesToServer = async () => {
- const links = await Networking.UploadFilesToServer(this.mediaFiles.map(file => ({ file })));
- console.log('finshed uploading', links.map(this.getServerPath));
- return (links ?? []).map(this.getServerPath);
- // this.setFileLinks((links ?? []).map(this.getServerPath));
+ /**
+ * 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(files);
+ this.setMediaFiles([...this.mediaFiles, ...files.map(file => ({ _id: v4(), file }))]);
};
- private reportComponent = () => {
- if (this.viewState === ViewState.VIEW) {
- return this.viewIssuesComponent();
- } else {
- return this.reportIssueComponent();
+ /**
+ * 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">
@@ -266,17 +334,23 @@ export class ReportManager extends React.Component<{}> {
required
/>
<div className="issues">
- {this.shownIssues
- .filter(issue => issue.title.toLowerCase().includes(this.query))
- .map(issue => (
- <IssueCard
- key={issue.number}
- issue={issue}
- onSelect={() => {
- this.setSelectedIssue(issue);
- }}
- />
- ))}
+ {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>
@@ -287,6 +361,9 @@ export class ReportManager extends React.Component<{}> {
);
};
+ /**
+ * @returns the form component for submitting issues
+ */
private reportIssueComponent = () => {
return (
<div className="report-issue">
@@ -337,12 +414,8 @@ export class ReportManager extends React.Component<{}> {
<Dropzone
onDrop={this.onDrop}
accept={{
- 'image/png': ['.png'],
- 'image/jpg': ['.jpg'],
- 'image/jpeg': ['.jpeg'],
- 'video/mp4': ['.mp4'],
- 'video/mpeg': ['.mpeg'],
- 'video/webm': ['.webm'],
+ 'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
+ 'video/*': ['.mp4', '.mpeg', '.webm', '.mov'],
'audio/mpeg': ['.mp3'],
'audio/wav': ['.wav'],
'audio/ogg': ['.ogg'],
@@ -357,24 +430,7 @@ export class ReportManager extends React.Component<{}> {
</div>
)}
</Dropzone>
- {this.mediaFiles.length > 0 && (
- <div className="files">
- <ul className="file-list">
- {this.mediaFiles.map((file, i) => (
- <li key={file.name} className="file-name">
- {file.name}
- <IconButton
- onClick={() => {
- this.setMediaFiles(this.mediaFiles.filter(f => f !== file));
- }}>
- <BiX />
- </IconButton>
- </li>
- ))}
- </ul>
- </div>
- )}
-
+ {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' }}
@@ -391,125 +447,32 @@ export class ReportManager extends React.Component<{}> {
);
};
- private get reportInterface() {
- const isReportingIssue = this.selectedIssue === null;
-
- return (
- <div className="settings-interface">
- <div className="issue-list-wrapper">
- <h3>Current Issues</h3>
- <input type="text" placeholder="search issues" onChange={e => this.setQuery(e.target.value)}></input>
- <br />
- {this.shownIssues.length === 0 ? (
- <ReactLoading className="loading-center" />
- ) : (
- this.shownIssues.map(issue => (
- <div className="issue-list" key={issue.number} onClick={() => this.setSelectedIssue(issue)}>
- {issue.title}
- </div>
- ))
- )}
- {/* <div className="settings-user">
- <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
- </div> */}
- </div>
-
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
- </div>
-
- <div className="issue-content" style={{ paddingTop: this.selectedIssue === undefined ? '50px' : 'inherit' }}>
- {this.selectedIssue === undefined ? 'no issue selected' : this.renderIssue(this.selectedIssue)}
- </div>
-
- <div className="report-issue-fab">
- <span className="report-disclaimer" hidden={!isReportingIssue}>
- Note: issue reporting is not anonymous.
- </span>
- <button onClick={() => (isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen())}>{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
- </div>
- </div>
- );
- }
-
- private renderIssue = (issue: Issue) => {
- const isReportingIssue = issue === null;
-
- return isReportingIssue ? (
- // report issue
- <div className="settings-content">
- <h3 style={{ textDecoration: 'underline' }}>Report an Issue</h3>
- <label>Please leave a title for the bug.</label>
- <br />
- <input value={this.bugTitle} type="text" placeholder="title" onChange={e => this.setBugTitle(e.target.value)} required />
- {/* <TextField fullWidth type="text" placeholder="Title..." required onChange={e => this.setBugTitle(e.target.value)} /> */}
- <br />
- <label>Please leave a description for the bug and how it can be recreated.</label>
- <textarea value={this.bugDescription} placeholder="description" onChange={e => this.setBugDescription(e.target.value)} required />
- <br />
- {/* {<label>Send to github issues? </label>
- <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
- <br /> } */}
-
- <label>Please label the issue</label>
- <div className="flex-select">
- <select name="bugType" onChange={e => (this.bugType = e.target.value)}>
- <option value="" disabled selected>
- Type
- </option>
- <option value="priority-low">Bug</option>
- <option value="priority-medium">Poor Design or Cosmetic</option>
- <option value="priority-high">Poor Documentation</option>
- </select>
-
- <select name="priority" onChange={e => (this.bugPriority = e.target.value)}>
- <option 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>
-
- <div>
- <label>Upload media that shows the bug (optional)</label>
- <input type="file" name="file" multiple accept="audio/*, video/*, image/*" onChange={e => this.uploadFiles(e.target)} />
- </div>
- <br />
-
- <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>
- {this.fileLinks === null ? 'Uploading...' : 'Submit'}
- </button>
- </div>
- ) : (
- // view issue
- <div className="issue-container">
- <h5 style={{ textAlign: 'left' }}>
- <a href={issue.html_url} target="_blank">
- Issue #{issue.number}
- </a>
- </h5>
- <div className="issue-title">{issue.title}</div>
- <ReactMarkdown children={issue.body as string} className="issue-body" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
- </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.reportInterface}
contents={this.reportComponent()}
isDisplayed={this.isOpen}
interactive={true}
closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: 'auto', minWidth: '400px', height: '85vh', maxHeight: '90vh', background: '#ffffff', borderRadius: '8px' }}
+ dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: '#ffffff', borderRadius: '8px' }}
/>
);
}
}
+// Mini components to render issues
+
interface IssueCardProps {
issue: Issue;
onSelect: () => void;
@@ -528,6 +491,48 @@ interface IssueViewProps {
}
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">
@@ -537,8 +542,7 @@ const IssueView = ({ issue }: IssueViewProps) => {
</a>
</span>
<h2 className="issue-title">{issue.title}</h2>
- <ReactMarkdown children={issue.body as string} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
- {/* <p className="issue-content">{issue.body}</p> */}
+ <ReactMarkdown children={issue.body ? parseBody(issue.body as string) : ''} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
</div>
);
};