From b1f189ffc7dfe558d5895c8f0cb103ab3e5c17d7 Mon Sep 17 00:00:00 2001 From: Sophie Zhang Date: Mon, 10 Jul 2023 19:10:34 -0400 Subject: filters and better ui --- src/client/util/reportManager/ReportManager.scss | 473 +++++++++++ src/client/util/reportManager/ReportManager.tsx | 570 +++++++++++++ .../util/reportManager/ReportManagerComponents.tsx | 153 ++++ .../util/reportManager/reportManagerSchema.ts | 877 +++++++++++++++++++++ .../util/reportManager/reportManagerUtils.ts | 54 ++ 5 files changed, 2127 insertions(+) create mode 100644 src/client/util/reportManager/ReportManager.scss create mode 100644 src/client/util/reportManager/ReportManager.tsx create mode 100644 src/client/util/reportManager/ReportManagerComponents.tsx create mode 100644 src/client/util/reportManager/reportManagerSchema.ts create mode 100644 src/client/util/reportManager/reportManagerUtils.ts (limited to 'src/client/util/reportManager') diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss new file mode 100644 index 000000000..81af41cb0 --- /dev/null +++ b/src/client/util/reportManager/ReportManager.scss @@ -0,0 +1,473 @@ +@import '../../views/global/globalCssVariables'; + +// header + +$text-gray: #64748b; +$outline-gray: #cbd5e1; + +.report-header { + display: flex; + justify-content: space-between; + align-items: center; + + h2 { + margin: 0; + padding: 0; + font-size: 24px; + } +} + +.report-header-vertical { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + + h2 { + margin: 0; + padding: 0; + padding-bottom: 8px; + font-size: 24px; + } +} + +// Report + +.report-issue { + width: 450px; + min-width: 300px; + padding: 16px; + padding-top: 32px; + display: flex; + flex-direction: column; + gap: 16px; + background-color: #ffffff; + text-align: left; + position: relative; + + .report-label { + font-size: 14px; + font-weight: 400; + color: $text-gray; + } + + .report-section { + display: flex; + flex-direction: column; + } + + .report-textarea { + width: 100%; + height: 80px; + padding: 8px; + resize: vertical; + // resize: none; + } + + .report-selects { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 16px; + + .report-select { + padding: 8px; + border-color: $outline-gray; + + .report-opt { + padding: 8px; + } + } + } +} + +.report-input { + border: none; + outline: none; + border-bottom: 1px solid $outline-gray; + padding: 8px; + padding-left: 0; + transition: all 0.2s ease; + + &:hover { + border-bottom-color: $text-gray; + } + &:focus { + border-bottom-color: #4476f7; + } +} + +// View issues + +.view-issues { + width: 75vw; + min-width: 500px; + display: flex; + gap: 16px; + height: 100%; + overflow-x: auto; + + video::-webkit-media-controls { + display: flex !important; + } + + .left { + flex: 1; + height: 100%; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; + background-color: #ffffff; + text-align: left; + position: relative; + + .issues { + padding-top: 24px; + position: relative; + flex-grow: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + gap: 16px; + } + } + + .right { + position: relative; + flex: 1; + padding: 16px; + min-width: 300px; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } +} + +// Issue + +.issue-card { + cursor: pointer; + padding: 16px; + background-color: #ffffff; + border: 1px solid $outline-gray; + transition: all 0.1s ease; + display: flex; + flex-direction: column; + gap: 8px; + border-radius: 8px; + transition: all 0.2s ease; + + .issue-top { + display: flex; + align-items: center; + gap: 16px; + padding-bottom: 8px; + } + + .issue-label { + cursor: pointer; + font-size: 14px; + font-weight: 400; + color: $text-gray; + padding: 0; + margin: 0; + } + + .issue-title { + font-size: 16px; + font-weight: 500; + padding: 0; + margin: 0; + color: #4476f7; + } + + &:hover { + background-color: #4476f7; + border-color: #4476f7; + color: #ffffff; + + .issue-label { + color: #ffffff; + } + + .issue-title { + color: #ffffff; + } + } +} + +// Dropzone + +.dropzone { + padding: 2rem; + border-radius: 0.5rem; + border: 2px dashed #f1f5f9; + + .dropzone-instructions { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + color: $text-gray; + + p { + text-align: center; + } + } +} + +.file-list { + box-sizing: border-box; + margin: 0; + padding: 0; + font-size: 14px; + color: $text-gray; + width: 100%; + overflow-x: auto; + list-style-type: none; + display: flex; + align-items: center; + gap: 16px; + + .file-name { + padding: 8px 12px; + display: flex; + align-items: center; + gap: 16px; + white-space: nowrap; + } +} + +// Detailed issue view + +.issue-view { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + background-color: #ffffff; + text-align: left; + position: relative; + overflow: auto; + + .issue-label { + color: $text-gray; + + .issue-link { + cursor: pointer; + color: #4476f7; + } + } + + .issue-title { + font-size: 24px; + margin: 0; + padding: 0; + } + + .issue-date { + font-size: 14px; + color: $text-gray; + } + + .issue-content { + font-size: 14px; + color: $text-gray; + } +} + +// tags flex lists + +.issues-filters { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + + .issues-filter { + display: flex; + gap: 8px; + align-items: center; + white-space: nowrap; + overflow-x: auto; + } +} + +.issue-tags { + display: flex; + gap: 8px; + align-items: center; + white-space: nowrap; + overflow-x: auto; +} + +// Media previews + +.report-media-wrapper { + position: relative; + cursor: pointer; + + .close-btn { + position: absolute; + top: 2px; + right: 2px; + opacity: 0; + } + + .report-media-content { + position: relative; + display: inline block; + + video::-webkit-media-controls { + display: flex !important; + } + } + + .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 */ + pointer-events: none; + + video::-webkit-media-controls { + pointer-events: all; + } + } + + &: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; + } +} + +// Tag styling + +.report-tag { + box-sizing: border-box; + padding: 4px 10px; + font-size: 10px; + border-radius: 32px; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} + +// Old code + +// <----------------------------------------------------------------------------> + +// .issue-list-wrapper { +// position: relative; +// min-width: 250px; +// background-color: $light-blue; +// overflow-y: scroll; +// } + +// .issue-list { +// display: flex; +// align-items: center; +// justify-content: space-between; +// padding: 5px; +// margin: 5px; +// border-radius: 5px; +// border: 1px solid grey; +// background-color: lightgoldenrodyellow; +// } + +// // issue should pop up when the user hover over the issue +// .issue-list:hover { +// box-shadow: 2px; +// cursor: pointer; +// border: 3px solid #252b33; +// } + +// .issue-content { +// background-color: white; +// padding: 10px; +// flex: 1 1 auto; +// overflow-y: scroll; +// } + +// .issue-title { +// font-size: 20px; +// font-weight: 600; +// color: black; +// } + +// .issue-body { +// padding: 0 10px; +// width: 100%; +// text-align: left; +// } + +// .issue-body > * { +// margin-top: 5px; +// } + +// .issue-body img, +// .issue-body video { +// display: block; +// max-width: 100%; +// } + +// .report-issue-fab { +// position: fixed; +// bottom: 20px; +// right: 20px; +// display: flex; +// align-items: center; +// justify-content: center; +// cursor: pointer; +// } + +// .loading-center { +// margin: auto 0; +// } + +// .settings-content label { +// margin-top: 10px; +// } + +// .report-disclaimer { +// font-size: 8px; +// color: grey; +// padding-right: 50px; +// font-style: italic; +// text-align: left; +// } + +// .flex-select { +// display: flex; +// align-items: center; +// justify-content: center; +// gap: 10px; +// } diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx new file mode 100644 index 000000000..ff17a5097 --- /dev/null +++ b/src/client/util/reportManager/ReportManager.tsx @@ -0,0 +1,570 @@ +import * as React from 'react'; +import '.././SettingsManager.scss'; +import './ReportManager.scss'; +import { action, observable } from 'mobx'; +import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs'; +import { CgClose } from 'react-icons/cg'; +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, Tooltip } from '@mui/material'; +import Dropzone from 'react-dropzone'; +import { theme } from '../../theme'; +import ReactLoading from 'react-loading'; +import v4 = require('uuid/v4'); +import { BugType, FileData, Priority, ViewState, inactiveBorderColor, inactiveColor } from './reportManagerUtils'; +import { IssueCard, IssueView, Tag } from './ReportManagerComponents'; +// import { IconButton } from 'browndash-components'; +const higflyout = require('@hig/flyout'); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +/** + * 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 + 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 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[]; + console.log(issues); + // 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); + } + }); + + constructor(props: {}) { + super(props); + ReportManager.Instance = this; + + // initializing Github connection + this.octokit = new Octokit({ + auth: process.env.GITHUB_ACCESS_TOKEN, + }); + } + + /** + * 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', + 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 { + 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; + } + console.log(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], + // }); + + // // 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 -> 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 => { + 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 }))]); + }; + + /** + * 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 <>; + }; + + 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; + }; + + /** + * @returns the component that dispays all issues + */ + private viewIssuesComponent = () => { + return ( +
+
+
+

Open Issues

+ +
+ { + this.setQuery(e.target.value); + }} + required + /> +
+
+ { + this.setPriorityFilter(null); + }} + fontSize="12px" + backgroundColor={this.priorityFilter === null ? theme.palette.primary.main : '#ffffff'} + color={this.priorityFilter === null ? '#ffffff' : inactiveColor} + border + borderColor={this.priorityFilter === null ? theme.palette.primary.main : inactiveBorderColor} + /> + {Object.values(Priority).map(p => { + return ( + { + this.setPriorityFilter(p); + }} + fontSize="12px" + backgroundColor={this.priorityFilter === p ? theme.palette.primary.main : '#ffffff'} + color={this.priorityFilter === p ? '#ffffff' : inactiveColor} + border + borderColor={this.priorityFilter === p ? theme.palette.primary.main : inactiveBorderColor} + /> + ); + })} +
+
+ { + this.setBugFilter(null); + }} + fontSize="12px" + backgroundColor={this.bugFilter === null ? theme.palette.primary.main : '#ffffff'} + color={this.bugFilter === null ? '#ffffff' : inactiveColor} + border + borderColor={this.bugFilter === null ? theme.palette.primary.main : inactiveBorderColor} + /> + {Object.values(BugType).map(b => { + return ( + { + this.setBugFilter(b); + }} + fontSize="12px" + backgroundColor={this.bugFilter === b ? theme.palette.primary.main : '#ffffff'} + color={this.bugFilter === b ? '#ffffff' : inactiveColor} + border + borderColor={this.bugFilter === b ? theme.palette.primary.main : inactiveBorderColor} + /> + ); + })} +
+
+
+ {this.fetchingIssues ? ( +
+ +
+ ) : ( + this.shownIssues + .filter(issue => issue.title.toLowerCase().includes(this.query)) + .filter(issue => this.passesTagFilter(issue)) + .map(issue => ( + { + this.setSelectedIssue(issue); + }} + /> + )) + )} +
+
+
{this.selectedIssue ? :
No issue selected
}
+
+ + { + e.stopPropagation(); + this.setRightExpanded(!this.rightExpanded); + }}> + {this.rightExpanded ? : } + + + + + +
+
+ ); + }; + + /** + * @returns the form component for submitting issues + */ + private reportIssueComponent = () => { + return ( +
+
+ +

Report an Issue

+
+
+ + this.setBugTitle(e.target.value)} required /> +
+
+ +