From fb939a1d2d59a776d9e3336dfc4a1e028ebc3113 Mon Sep 17 00:00:00 2001 From: Sophie Zhang Date: Sun, 9 Jul 2023 00:59:10 -0400 Subject: Almost done --- src/client/Network.ts | 16 +- src/client/util/ReportManager.scss | 98 ++++++-- src/client/util/ReportManager.tsx | 468 +++++++++++++++++++------------------ 3 files changed, 324 insertions(+), 258 deletions(-) (limited to 'src') 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(fileguidpairs: FileGuidPair | FileGuidPair[]): Promise[]> { + export async function UploadFilesToServer(fileguidpairs: FileGuidPair | FileGuidPair[], browndash?: boolean): Promise[]> { 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 { 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 { 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 => { + 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 ( +
+
+ {`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 (
@@ -266,17 +334,23 @@ export class ReportManager extends React.Component<{}> { required />
- {this.shownIssues - .filter(issue => issue.title.toLowerCase().includes(this.query)) - .map(issue => ( - { - this.setSelectedIssue(issue); - }} - /> - ))} + {this.fetchingIssues ? ( +
+ +
+ ) : ( + this.shownIssues + .filter(issue => issue.title.toLowerCase().includes(this.query)) + .map(issue => ( + { + this.setSelectedIssue(issue); + }} + /> + )) + )}
{this.selectedIssue ? :
No issue selected
}
@@ -287,6 +361,9 @@ export class ReportManager extends React.Component<{}> { ); }; + /** + * @returns the form component for submitting issues + */ private reportIssueComponent = () => { return (
@@ -337,12 +414,8 @@ export class ReportManager extends React.Component<{}> { {
)} - {this.mediaFiles.length > 0 && ( -
-
    - {this.mediaFiles.map((file, i) => ( -
  • - {file.name} - { - this.setMediaFiles(this.mediaFiles.filter(f => f !== file)); - }}> - - -
  • - ))} -
-
- )} - + {this.mediaFiles.length > 0 &&
    {this.mediaFiles.map(file => this.getMediaPreview(file))}
} - */} - - -
- -
- -
- {this.selectedIssue === undefined ? 'no issue selected' : this.renderIssue(this.selectedIssue)} -
- -
- - -
- - ); - } - - private renderIssue = (issue: Issue) => { - const isReportingIssue = issue === null; - - return isReportingIssue ? ( - // report issue -
-

Report an Issue

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