aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-07-19 19:11:06 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-07-19 19:11:06 -0400
commitea217200f1c42e4d4b142abc9abd55ca49535c49 (patch)
tree3f8974dfdc0f63ea0549d7681b74c5b08bcfc1d3 /src
parent77b26f2dbdc2f3df0ab65aa3053854b4a24c586f (diff)
lots of changes, just need server endpoint before pull
Diffstat (limited to 'src')
-rw-r--r--src/client/Network.ts3
-rw-r--r--src/client/util/reportManager/ReportManager.scss10
-rw-r--r--src/client/util/reportManager/ReportManager.tsx382
-rw-r--r--src/client/util/reportManager/ReportManagerComponents.tsx137
-rw-r--r--src/client/util/reportManager/reportManagerUtils.ts181
-rw-r--r--src/client/views/PropertiesButtons.tsx90
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}