diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-19 19:11:06 -0400 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-19 19:11:06 -0400 |
commit | ea217200f1c42e4d4b142abc9abd55ca49535c49 (patch) | |
tree | 3f8974dfdc0f63ea0549d7681b74c5b08bcfc1d3 /src | |
parent | 77b26f2dbdc2f3df0ab65aa3053854b4a24c586f (diff) |
lots of changes, just need server endpoint before pull
Diffstat (limited to 'src')
-rw-r--r-- | src/client/Network.ts | 3 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManager.scss | 10 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManager.tsx | 382 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManagerComponents.tsx | 137 | ||||
-rw-r--r-- | src/client/util/reportManager/reportManagerUtils.ts | 181 | ||||
-rw-r--r-- | src/client/views/PropertiesButtons.tsx | 90 |
6 files changed, 445 insertions, 358 deletions
diff --git a/src/client/Network.ts b/src/client/Network.ts index 70b51d036..39bf69e32 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -38,6 +38,7 @@ export namespace Networking { * with the mapping of guid to files as parameters. * * @param fileguidpairs the files and corresponding guids to be uploaded to the server + * @param browndash whether the endpoint should be invoked on the browndash server * @returns the response as a json from the server */ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[], browndash?: boolean): Promise<Upload.FileResponse<T>[]> { @@ -68,7 +69,7 @@ export namespace Networking { body: formData, }; - const endpoint = browndash ? 'http://10.38.71.246:1050/uploadFormData' : '/uploadFormData'; + const endpoint = browndash ? '[insert endpoint allowing local => browndash]' : '/uploadFormData'; const response = await fetch(endpoint, parameters); return response.json(); } diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss index 4e80cbeeb..cd6a1d934 100644 --- a/src/client/util/reportManager/ReportManager.scss +++ b/src/client/util/reportManager/ReportManager.scss @@ -7,6 +7,12 @@ justify-content: space-between; align-items: center; + .header-btns { + display: flex; + align-items: center; + gap: 0.5rem; + } + h2 { margin: 0; padding: 0; @@ -53,12 +59,14 @@ } .report-textarea { + border: none; + outline: none; width: 100%; height: 80px; padding: 8px; resize: vertical; background: transparent; - // resize: none; + transition: border 0.3s ease; } .report-selects { 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> diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx index 651442030..8f882c7f2 100644 --- a/src/client/util/reportManager/ReportManagerComponents.tsx +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Issue } from './reportManagerSchema'; -import { darkColors, getLabelColors, isLightText, lightColors } from './reportManagerUtils'; +import { darkColors, dashBlue, getLabelColors, isDarkMode, lightColors } from './reportManagerUtils'; import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; @@ -8,9 +8,56 @@ import { StrCast } from '../../../fields/Types'; import { Doc } from '../../../fields/Doc'; /** - * Mini components to render issues. + * Mini helper components for the report component. */ +interface FilterProps<T> { + items: T[]; + activeValue: T | null; + setActiveValue: (val: T | null) => void; +} + +// filter ui for issues (horizontal list of tags) +export const Filter = <T extends string>({ items, activeValue, setActiveValue }: FilterProps<T>) => { + // establishing theme + const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)); + const colors = darkMode ? darkColors : lightColors; + const isTagDarkMode = isDarkMode(StrCast(Doc.UserDoc().userColor)); + const activeTagTextColor = isTagDarkMode ? darkColors.text : lightColors.text; + + return ( + <div className="issues-filter"> + <Tag + text={'All'} + onClick={() => { + setActiveValue(null); + }} + fontSize="12px" + backgroundColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : 'transparent'} + color={activeValue === null ? activeTagTextColor : colors.textGrey} + borderColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : colors.border} + border + /> + {items.map(item => { + return ( + <Tag + key={item} + text={item} + onClick={() => { + setActiveValue(item); + }} + fontSize="12px" + backgroundColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : 'transparent'} + color={activeValue === item ? activeTagTextColor : colors.textGrey} + border + borderColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : colors.border} + /> + ); + })} + </div> + ); +}; + interface IssueCardProps { issue: Issue; onSelect: () => void; @@ -19,11 +66,11 @@ interface IssueCardProps { // Component for the issue cards list on the left export const IssueCard = ({ issue, onSelect }: IssueCardProps) => { const [textColor, setTextColor] = React.useState(''); - const [bgColor, setBgColor] = React.useState(''); - const [borderColor, setBorderColor] = React.useState(''); + const [bgColor, setBgColor] = React.useState('transparent'); + const [borderColor, setBorderColor] = React.useState('transparent'); const resetColors = () => { - const darkMode = isLightText(StrCast(Doc.UserDoc().userBackgroundColor)); + const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)); const colors = darkMode ? darkColors : lightColors; setTextColor(colors.text); setBorderColor(colors.border); @@ -31,10 +78,10 @@ export const IssueCard = ({ issue, onSelect }: IssueCardProps) => { }; const handlePointerOver = () => { - const darkMode = isLightText(StrCast(Doc.UserDoc().userVariantColor)); + const darkMode = isDarkMode(StrCast(Doc.UserDoc().userColor)); setTextColor(darkMode ? darkColors.text : lightColors.text); - setBorderColor(StrCast(Doc.UserDoc().userVariantColor)); - setBgColor(StrCast(Doc.UserDoc().userVariantColor)); + setBorderColor(StrCast(Doc.UserDoc().userColor)); + setBgColor(StrCast(Doc.UserDoc().userColor)); }; React.useEffect(() => { @@ -165,10 +212,6 @@ export const IssueView = ({ issue }: IssueViewProps) => { const validPromise: Promise<boolean> = new Promise(resolve => { imgElement.addEventListener('load', () => resolve(true)); imgElement.addEventListener('error', () => resolve(false)); - // if taking too long to load, return prematurely (when the browndash server is down) - // setTimeout(() => { - // resolve(false); - // }, 1500); }); imgElement.src = src; return validPromise; @@ -180,9 +223,6 @@ export const IssueView = ({ issue }: IssueViewProps) => { const validPromise: Promise<boolean> = new Promise(resolve => { videoElement.addEventListener('loadeddata', () => resolve(true)); videoElement.addEventListener('error', () => resolve(false)); - // setTimeout(() => { - // resolve(false); - // }, 1500); }); videoElement.src = src; return validPromise; @@ -194,9 +234,6 @@ export const IssueView = ({ issue }: IssueViewProps) => { const validPromise: Promise<boolean> = new Promise(resolve => { audioElement.addEventListener('loadeddata', () => resolve(true)); audioElement.addEventListener('error', () => resolve(false)); - // setTimeout(() => { - // resolve(false); - // }, 1500); }); audioElement.src = src; return validPromise; @@ -257,3 +294,67 @@ export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColo </div> ); }; + +interface FormInputProps { + value: string; + placeholder: string; + onChange: (val: string) => void; +} +export const FormInput = ({ value, placeholder, onChange }: FormInputProps) => { + const [inputBorderColor, setInputBorderColor] = React.useState(''); + + return ( + <input + className="report-input" + style={{ borderBottom: `1px solid ${inputBorderColor}` }} + value={value} + type="text" + placeholder={placeholder} + onChange={e => onChange(e.target.value)} + required + onPointerOver={() => { + if (inputBorderColor === dashBlue) return; + setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey); + }} + onPointerOut={() => { + if (inputBorderColor === dashBlue) return; + setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border); + }} + onFocus={() => { + setInputBorderColor(dashBlue); + }} + onBlur={() => { + setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border); + }} + /> + ); +}; + +export const FormTextArea = ({ value, placeholder, onChange }: FormInputProps) => { + const [textAreaBorderColor, setTextAreaBorderColor] = React.useState(''); + + return ( + <textarea + className="report-textarea" + value={value} + placeholder={placeholder} + onChange={e => onChange(e.target.value)} + required + style={{ border: `1px solid ${textAreaBorderColor}` }} + onPointerOver={() => { + if (textAreaBorderColor === dashBlue) return; + setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey); + }} + onPointerOut={() => { + if (textAreaBorderColor === dashBlue) return; + setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border); + }} + onFocus={() => { + setTextAreaBorderColor(dashBlue); + }} + onBlur={() => { + setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border); + }} + /> + ); +}; diff --git a/src/client/util/reportManager/reportManagerUtils.ts b/src/client/util/reportManager/reportManagerUtils.ts index 682113a89..d8344220f 100644 --- a/src/client/util/reportManager/reportManagerUtils.ts +++ b/src/client/util/reportManager/reportManagerUtils.ts @@ -1,5 +1,11 @@ // Final file url reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" /> +import { Octokit } from '@octokit/core'; +import { Networking } from '../../Network'; +import { Issue } from './reportManagerSchema'; + +// enums and interfaces + export enum ViewState { VIEW, CREATE, @@ -23,6 +29,174 @@ export interface FileData { file: File; } +export interface ReportForm { + title: string; + description: string; + type: BugType; + priority: Priority; + mediaFiles: FileData[]; +} + +export type ReportFormKey = keyof ReportForm; + +export const emptyReportForm = { + title: '', + description: '', + type: BugType.BUG, + priority: Priority.MEDIUM, + mediaFiles: [], +}; + +// interfacing with Github + +/** + * Fetches issues from Github. + * @returns array of all issues + */ +export const getAllIssues = async (octokit: Octokit): Promise<any[]> => { + const res = await 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'); + } +}; + +/** + * Formats issue title. + * + * @param title title of issue + * @param userEmail email of issue submitter + * @returns formatted title + */ +export const formatTitle = (title: string, userEmail: string): string => `${title} - ${userEmail.replace('@brown.edu', '')}`; + +// uploading + +// 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 +export const fileLinktoServerLink = (fileLink: string): 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 + */ +export const 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 + */ +export const uploadFilesToServer = async (mediaFiles: FileData[]): Promise<string[] | undefined> => { + try { + // need to always upload to browndash + const links = await Networking.UploadFilesToServer( + mediaFiles.map(file => ({ file: file.file })), + true + ); + return (links ?? []).map(getServerPath).map(fileLinktoServerLink); + } catch (err) { + if (err instanceof Error) { + alert(err.message); + } else { + alert(err); + } + } +}; + +// helper functions + +/** + * Returns when the issue passes the current filters. + * + * @param issue issue to check + * @returns boolean indicating whether the issue passes the current filters + */ +export const passesTagFilter = (issue: Issue, priorityFilter: string | null, bugFilter: string | null) => { + let passesPriority = true; + let passesBug = true; + if (priorityFilter) { + passesPriority = issue.labels.some(label => { + if (typeof label === 'string') { + return label === priorityFilter; + } else { + return label.name === priorityFilter; + } + }); + } + if (bugFilter) { + passesBug = issue.labels.some(label => { + if (typeof label === 'string') { + return label === bugFilter; + } else { + return label.name === bugFilter; + } + }); + } + return passesPriority && passesBug; +}; + +// sets and lists + +export const prioritySet = new Set(Object.values(Priority)); +export const bugSet = new Set(Object.values(BugType)); + +export const priorityDropdownItems = [ + { + text: 'Low', + val: Priority.LOW, + }, + { + text: 'Medium', + val: Priority.MEDIUM, + }, + { + text: 'High', + val: Priority.HIGH, + }, +]; + +export const bugDropdownItems = [ + { + text: 'Bug', + val: BugType.BUG, + }, + { + text: 'Poor Design or Cosmetic', + val: BugType.COSMETIC, + }, + { + text: 'Documentation', + val: BugType.DOCUMENTATION, + }, + { + text: 'New feature or request', + val: BugType.ENHANCEMENT, + }, +]; + +// colors + // [bgColor, color] export const priorityColors: { [key: string]: string[] } = { 'priority-low': ['#d4e0ff', '#000000'], @@ -38,9 +212,6 @@ export const bugColors: { [key: string]: string[] } = { enhancement: ['#36d4f0', '#ffffff'], }; -export const prioritySet = new Set(Object.values(Priority)); -export const bugSet = new Set(Object.values(BugType)); - export const getLabelColors = (label: string): string[] => { if (prioritySet.has(label as Priority)) { return priorityColors[label]; @@ -66,7 +237,7 @@ const hexToRgb = (hex: string) => { }; // function that returns whether text should be light on the given bg color -export const isLightText = (bgHex: string): boolean => { +export const isDarkMode = (bgHex: string): boolean => { const { r, g, b } = hexToRgb(bgHex); return r * 0.299 + g * 0.587 + b * 0.114 <= 186; }; @@ -82,3 +253,5 @@ export const darkColors = { textGrey: '#d6d6d6', border: '#717171', }; + +export const dashBlue = '#4476f7'; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 6105cc1b5..3a3b2fc38 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -56,24 +56,25 @@ export class PropertiesButtons extends React.Component<{}, {}> { propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedDoc; const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? false : true); - return !targetDoc ? null : <Toggle - toggleStatus={BoolCast(targetDoc[property])} - text={label(targetDoc?.[property])} - color={StrCast(Doc.UserDoc().userColor)} - icon={icon(targetDoc?.[property] as any)} - iconPlacement={'left'} - align={'flex-start'} - fillWidth={true} - toggleType={ToggleType.BUTTON} - onClick={undoable(() => { - if (SelectionManager.Views().length > 1) { - SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); - } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); - }, property)} - /> + return !targetDoc ? null : ( + <Toggle + toggleStatus={BoolCast(targetDoc[property])} + text={label(targetDoc?.[property])} + color={StrCast(Doc.UserDoc().userColor)} + icon={icon(targetDoc?.[property] as any)} + iconPlacement={'left'} + align={'flex-start'} + fillWidth={true} + toggleType={ToggleType.BUTTON} + onClick={undoable(() => { + if (SelectionManager.Views().length > 1) { + SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); + } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); + }, property)} + /> + ); }; - // this implments a container pattern by marking the targetDoc (collection) as a lightbox // that always fits its contents to its container and that hides all other documents when // a link is followed that targets a 'lightbox' destination @@ -148,7 +149,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get clustersButton() { return this.propertyToggleBtn( - on => (on ?'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'), + on => (on ? 'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'), '_freeform_useClusters', on => `${on ? 'Hide' : 'Show'} clusters`, on => <FaBraille /> @@ -163,12 +164,13 @@ export class PropertiesButtons extends React.Component<{}, {}> { ); } - @computed get forceActiveButton() { //select text + @computed get forceActiveButton() { + //select text return this.propertyToggleBtn( - on => on ? 'INACTIVE INTERACTION' : 'ACTIVE INTERACTION', + on => (on ? 'INACTIVE INTERACTION' : 'ACTIVE INTERACTION'), '_forceActive', on => `${on ? 'Select to activate' : 'Contents always active'} `, - on => <MdTouchApp/> // 'eye' + on => <MdTouchApp /> // 'eye' ); } @@ -218,7 +220,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { } @computed get captionButton() { - return this.propertyToggleBtn( //DEVELOPER + return this.propertyToggleBtn( + //DEVELOPER on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), //'Caption', '_layout_showCaption', on => `${on ? 'Hide' : 'Show'} caption footer`, @@ -227,17 +230,19 @@ export class PropertiesButtons extends React.Component<{}, {}> { ); } - @computed get chromeButton() { // developer -- removing UI decoration + @computed get chromeButton() { + // developer -- removing UI decoration return this.propertyToggleBtn( - on => on ? 'ENABLE UI CONTROLS' : 'DISABLE UI CONTROLS', + on => (on ? 'ENABLE UI CONTROLS' : 'DISABLE UI CONTROLS'), '_chromeHidden', on => `${on ? 'Show' : 'Hide'} editing UI`, - on => on? <TbEditCircle/> : <TbEditCircleOff/> , // 'edit', + on => (on ? <TbEditCircle /> : <TbEditCircleOff />), // 'edit', (dv, doc) => ((dv?.rootDoc || doc)._chromeHidden = !(dv?.rootDoc || doc)._chromeHidden) ); } - @computed get layout_autoHeightButton() { // store previous dimensions to store old values + @computed get layout_autoHeightButton() { + // store previous dimensions to store old values return this.propertyToggleBtn( on => 'Auto\xA0Size', '_layout_autoHeight', @@ -251,7 +256,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => (on ? 'HIDE GRID' : 'DISPLAY GRID'), '_freeform_backgroundGrid', on => `Display background grid in collection`, - on => (on ? <MdGridOff /> :<MdGridOn /> ) //'border-all' + on => (on ? <MdGridOff /> : <MdGridOn />) //'border-all' ); } @@ -288,13 +293,14 @@ export class PropertiesButtons extends React.Component<{}, {}> { // } // ); // } - @computed get snapButton() { // THESE ARE NOT COMING + @computed get snapButton() { + // THESE ARE NOT COMING return this.propertyToggleBtn( on => (on ? 'HIDE SNAP LINES' : 'SHOW SNAP LINES'), 'freeform_snapLines', on => `Display snapping lines when objects are dragged`, on => <TfiBarChart />, //'th', - undefined, + undefined ); } @@ -346,11 +352,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { const followLoc = this.selectedDoc._followLinkLocation; const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); - if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace' - else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight' - else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal' - else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail' - else return 'nothing' + if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace'; + else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight'; + else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal'; + else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail'; + else return 'nothing'; } @computed @@ -362,22 +368,20 @@ export class PropertiesButtons extends React.Component<{}, {}> { ['linkInPlace', 'Open Link in Lightbox'], ['linkOnRight', 'Open Link on Right'], ]; - + const items: IListItemProps[] = buttonList.map(value => { - return ( - { - text: value[1], - val: value[1], - } - ); + return { + text: value[1], + val: value[1], + }; }); - console.log("click val: ", this.onClickVal) + console.log('click val: ', this.onClickVal); return !this.selectedDoc ? null : ( <Dropdown - tooltip={'Choose onClick behavior'} + tooltip={'Choose onClick behavior'} items={items} selectedVal={this.onClickVal} - setSelectedVal={(val) => this.handleOptionChange(val as string)} + setSelectedVal={val => this.handleOptionChange(val as string)} title={'Choose onClick behaviour'} color={StrCast(Doc.UserDoc().userColor)} dropdownType={DropdownType.SELECT} |