diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/theme.ts | 8 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManager.scss (renamed from src/client/util/ReportManager.scss) | 114 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManager.tsx (renamed from src/client/util/ReportManager.tsx) | 304 | ||||
-rw-r--r-- | src/client/util/reportManager/ReportManagerComponents.tsx | 153 | ||||
-rw-r--r-- | src/client/util/reportManager/reportManagerSchema.ts (renamed from src/client/util/reportManagerSchema.ts) | 2 | ||||
-rw-r--r-- | src/client/util/reportManager/reportManagerUtils.ts | 54 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/LoadingBox.scss | 4 | ||||
-rw-r--r-- | src/client/views/topbar/TopBar.tsx | 2 |
10 files changed, 477 insertions, 169 deletions
diff --git a/src/client/theme.ts b/src/client/theme.ts index f0a11fe6e..0ef2e5e2c 100644 --- a/src/client/theme.ts +++ b/src/client/theme.ts @@ -19,5 +19,13 @@ export const theme = createTheme({ }, }, }, + + MuiTooltip: { + styleOverrides: { + tooltip: { + fontSize: '12px', + }, + }, + }, }, }); diff --git a/src/client/util/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss index b3bd998e6..81af41cb0 100644 --- a/src/client/util/ReportManager.scss +++ b/src/client/util/reportManager/ReportManager.scss @@ -1,7 +1,10 @@ -@import '../views/global/globalCssVariables'; +@import '../../views/global/globalCssVariables'; // header +$text-gray: #64748b; +$outline-gray: #cbd5e1; + .report-header { display: flex; justify-content: space-between; @@ -45,7 +48,7 @@ .report-label { font-size: 14px; font-weight: 400; - color: #747474; + color: $text-gray; } .report-section { @@ -57,7 +60,8 @@ width: 100%; height: 80px; padding: 8px; - resize: none; + resize: vertical; + // resize: none; } .report-selects { @@ -68,7 +72,7 @@ .report-select { padding: 8px; - border-color: #c6c6c6; + border-color: $outline-gray; .report-opt { padding: 8px; @@ -80,13 +84,13 @@ .report-input { border: none; outline: none; - border-bottom: 1px solid #c6c6c6; + border-bottom: 1px solid $outline-gray; padding: 8px; padding-left: 0; transition: all 0.2s ease; &:hover { - border-bottom-color: #7f7f7f; + border-bottom-color: $text-gray; } &:focus { border-bottom-color: #4476f7; @@ -96,15 +100,20 @@ // View issues .view-issues { - width: 65vw; + width: 75vw; min-width: 500px; display: flex; gap: 16px; height: 100%; + overflow-x: auto; + + video::-webkit-media-controls { + display: flex !important; + } .left { - height: 100%; flex: 1; + height: 100%; padding: 16px; display: flex; flex-direction: column; @@ -114,6 +123,7 @@ position: relative; .issues { + padding-top: 24px; position: relative; flex-grow: 1; overflow-y: auto; @@ -125,43 +135,47 @@ } .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; - flex: 1; } } -// video - -.default-video::-webkit-media-controls { - display: block !important; -} - // Issue -.issue { +.issue-card { cursor: pointer; padding: 16px; background-color: #ffffff; - border: 1px solid #d3d3d3; + 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; - // box-shadow: 0 0 8px #d0d0d07c; + + .issue-top { + display: flex; + align-items: center; + gap: 16px; + padding-bottom: 8px; + } .issue-label { cursor: pointer; font-size: 14px; font-weight: 400; - color: #7f7f7f; + color: $text-gray; + padding: 0; + margin: 0; } .issue-title { @@ -192,14 +206,14 @@ .dropzone { padding: 2rem; border-radius: 0.5rem; - border: 2px dashed #ebebeb; + border: 2px dashed #f1f5f9; .dropzone-instructions { display: flex; flex-direction: column; align-items: center; gap: 16px; - color: #7f7f7f; + color: $text-gray; p { text-align: center; @@ -212,11 +226,12 @@ margin: 0; padding: 0; font-size: 14px; - color: #7f7f7f; + color: $text-gray; width: 100%; overflow-x: auto; list-style-type: none; display: flex; + align-items: center; gap: 16px; .file-name { @@ -242,7 +257,7 @@ overflow: auto; .issue-label { - color: #7f7f7f; + color: $text-gray; .issue-link { cursor: pointer; @@ -256,16 +271,47 @@ padding: 0; } + .issue-date { + font-size: 14px; + color: $text-gray; + } + .issue-content { font-size: 14px; - color: #7f7f7f; + 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; @@ -277,7 +323,10 @@ .report-media-content { position: relative; display: inline block; - cursor: pointer; + + video::-webkit-media-controls { + display: flex !important; + } } .report-media-content::after { @@ -290,6 +339,11 @@ 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 { @@ -317,6 +371,16 @@ } } +// 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 // <----------------------------------------------------------------------------> diff --git a/src/client/util/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 125d20876..ff17a5097 100644 --- a/src/client/util/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,52 +1,29 @@ -import { ColorState, SketchPicker } from 'react-color'; -import { Id } from '../../fields/FieldSymbols'; -import { BoolCast, Cast, StrCast } from '../../fields/Types'; -import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; -import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; -import { DocServer } from '../DocServer'; -import { FontIconBox } from '../views/nodes/button/FontIconBox'; -import { DragManager } from './DragManager'; -import { GroupManager } from './GroupManager'; -import { CheckBox } from '../views/search/CheckBox'; -import { undoBatch } from './UndoManager'; import * as React from 'react'; -import './SettingsManager.scss'; +import '.././SettingsManager.scss'; import './ReportManager.scss'; -import { action, computed, observable, runInAction } from 'mobx'; -import { BsX } from 'react-icons/bs'; -import { BiX } from 'react-icons/bi'; +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 { Doc } from '../../../fields/Doc'; +import { Networking } from '../../Network'; +import { MainViewModal } from '../../views/MainViewModal'; import { Octokit } from '@octokit/core'; -import { Button, IconButton } from '@mui/material'; -import { Oval } from 'react-loader-spinner'; +import { Button, IconButton, Tooltip } from '@mui/material'; import Dropzone from 'react-dropzone'; -import ReactMarkdown from 'react-markdown'; -import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import { theme } from '../theme'; +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; -enum ViewState { - VIEW, - CREATE, -} - -interface FileData { - _id: string; - file: File; -} - -// Format reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" /> - /** * Class for reporting and viewing Github issues within the app. */ @@ -82,11 +59,28 @@ export class ReportManager extends React.Component<{}> { 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 = ''; @@ -119,6 +113,8 @@ export class ReportManager extends React.Component<{}> { 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); @@ -133,7 +129,7 @@ export class ReportManager extends React.Component<{}> { // initializing Github connection this.octokit = new Octokit({ - auth: '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', + auth: process.env.GITHUB_ACCESS_TOKEN, }); } @@ -145,6 +141,7 @@ export class ReportManager extends React.Component<{}> { const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', { owner: 'brown-dash', repo: 'Dash-Web', + per_page: 80, }); // 200 status means success @@ -171,21 +168,22 @@ export class ReportManager extends React.Component<{}> { // 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], - }); + // 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; - } + // // 201 status means success + // if (req.status !== 201) { + // alert('Error creating issue on github.'); + // return; + // } // Reset fields this.setBugTitle(''); @@ -207,7 +205,7 @@ export class ReportManager extends React.Component<{}> { */ private formatTitle = (title: string, userEmail: string): string => `${title} - ${userEmail.replace('@brown.edu', '')}`; - // turns an upload link into a servable link + // 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 @@ -237,6 +235,7 @@ export class ReportManager extends React.Component<{}> { */ 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 @@ -273,7 +272,7 @@ export class ReportManager extends React.Component<{}> { return ( <div key={fileData._id} className="report-media-wrapper"> <div className="report-media-content"> - <img height={50} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} /> + <img height={100} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} /> </div> <IconButton onClick={() => this.setMediaFiles(this.mediaFiles.filter(f => f._id !== fileData._id))} className="close-btn"> <BsX color="#ffffff" /> @@ -284,7 +283,7 @@ export class ReportManager extends React.Component<{}> { return ( <div key={fileData._id} className="report-media-wrapper"> <div className="report-media-content"> - <video controls style={{ height: '50px', width: 'auto', display: 'block' }}> + <video className="report-default-video" controls style={{ height: '100px', width: 'auto', display: 'block' }}> <source src={preview} type="video/mp4" /> Your browser does not support the video tag. </video> @@ -307,13 +306,37 @@ export class ReportManager extends React.Component<{}> { 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 ( <div className="view-issues"> - <div className="left"> + <div className="left" style={{ display: this.rightExpanded ? 'none' : 'flex' }}> <div className="report-header"> <h2>Open Issues</h2> <Button @@ -327,20 +350,81 @@ export class ReportManager extends React.Component<{}> { <input className="report-input" type="text" - placeholder="Filter..." + 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 ? 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 ( + <Tag + key={p} + text={p} + onClick={() => { + 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} + /> + ); + })} + </div> + <div className="issues-filter"> + <Tag + text={'All'} + onClick={() => { + 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 ( + <Tag + key={b} + text={b} + onClick={() => { + 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} + /> + ); + })} + </div> + </div> <div className="issues"> {this.fetchingIssues ? ( <div style={{ flexGrow: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}> - <Oval height={50} width={50} color={theme.palette.primary.main} visible={true} ariaLabel="oval-loading" secondaryColor={theme.palette.primary.main + 'b8'} strokeWidth={3} strokeWidthSecondary={3} /> + <ReactLoading type="spin" color={theme.palette.primary.main} width={50} height={50} /> </div> ) : ( this.shownIssues .filter(issue => issue.title.toLowerCase().includes(this.query)) + .filter(issue => this.passesTagFilter(issue)) .map(issue => ( <IssueCard key={issue.number} @@ -353,10 +437,21 @@ export class ReportManager extends React.Component<{}> { )} </div> </div> - <div className="right">{this.selectedIssue ? <IssueView issue={this.selectedIssue} /> : <div>No issue selected</div>}</div> - <IconButton sx={{ position: 'absolute', top: '4px', right: '4px' }} onClick={this.close}> - <BiX size="16px" /> - </IconButton> + <div className="right">{this.selectedIssue ? <IssueView issue={this.selectedIssue} /> : <div>No issue selected</div>} </div> + <div style={{ position: 'absolute', top: '8px', right: '8px', display: 'flex', gap: '16px' }}> + <Tooltip title={this.rightExpanded ? 'Minimize right side' : 'Expand right side'}> + <IconButton + onClick={e => { + e.stopPropagation(); + this.setRightExpanded(!this.rightExpanded); + }}> + {this.rightExpanded ? <BsArrowsAngleContract size="16px" /> : <BsArrowsAngleExpand size="16px" />} + </IconButton> + </Tooltip> + <IconButton onClick={this.close}> + <CgClose size="16px" /> + </IconButton> + </div> </div> ); }; @@ -392,15 +487,18 @@ export class ReportManager extends React.Component<{}> { <option value="" disabled selected> Type </option> - <option className="report-opt" value="priority-low"> + <option className="report-opt" value="bug"> Bug </option> - <option className="report-opt" value="priority-medium"> + <option className="report-opt" value="cosmetic"> Poor Design or Cosmetic </option> - <option className="report-opt" value="priority-high"> + <option className="report-opt" value="documentation"> Poor Documentation </option> + <option className="report-opt" value="enhancement"> + New feature or request + </option> </select> <select className="report-select" name="bigPriority" onChange={e => (this.bugPriority = e.target.value)}> <option className="report-opt" value="" disabled selected> @@ -438,10 +536,10 @@ export class ReportManager extends React.Component<{}> { this.reportIssue(); }}> Submit - {this.submitting && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff87" strokeWidth={3} strokeWidthSecondary={3} />} + {this.submitting && <ReactLoading type="spin" color={theme.palette.primary.main} width={20} height={20} />} </Button> - <IconButton sx={{ position: 'absolute', top: '4px', right: '4px' }} onClick={this.close}> - <BiX size={'16px'} /> + <IconButton onClick={this.close} sx={{ position: 'absolute', top: '4px', right: '4px' }}> + <CgClose size={'16px'} /> </IconButton> </div> ); @@ -470,79 +568,3 @@ export class ReportManager extends React.Component<{}> { ); } } - -// Mini components to render issues - -interface IssueCardProps { - issue: Issue; - onSelect: () => void; -} -const IssueCard = ({ issue, onSelect }: IssueCardProps) => { - return ( - <div className="issue" onClick={onSelect}> - <label className="issue-label">#{issue.number}</label> - <h3 className="issue-title">{issue.title}</h3> - </div> - ); -}; - -interface IssueViewProps { - issue: Issue; -} - -const IssueView = ({ issue }: IssueViewProps) => { - const parseBody = (body: string) => { - const imgTagRegex = /<img\b[^>]*\/?>/; - const fileRegex = /https:\/\/browndash\.com\/files/; - const parts = body.split('\n'); - - const modifiedParts = parts.map(part => { - if (imgTagRegex.test(part)) { - return getLinkFromTag(part); - } else if (fileRegex.test(part)) { - return getTagFromUrl(part); - } else { - return part; - } - }); - return modifiedParts.join('\n'); - }; - - const getLinkFromTag = (tag: string) => { - const regex = /src="([^"]+)"/; - let url = ''; - const match = tag.match(regex); - if (match) { - url = match[1]; - } - - return `\n${url}`; - }; - - const getTagFromUrl = (url: string) => { - const imgRegex = /https:\/\/browndash\.com\/files[/\\]images/; - const videoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; - const audioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; - - if (imgRegex.test(url)) { - return `${url}\n<img width="100%" alt="Issue asset" src=${url} />`; - } else if (videoRegex.test(url)) { - return url; - } else if (audioRegex.test(url)) { - return `${url}\n<audio src=${url} controls />`; - } - }; - - return ( - <div className="issue-view"> - <span className="issue-label"> - Issue{' '} - <a className="issue-link" href={issue.html_url} target="_blank"> - #{issue.number} - </a> - </span> - <h2 className="issue-title">{issue.title}</h2> - <ReactMarkdown children={issue.body ? parseBody(issue.body as string) : ''} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> - </div> - ); -}; diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx new file mode 100644 index 000000000..1a4ddb3a3 --- /dev/null +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { Issue } from './reportManagerSchema'; +import { getLabelColors } from './reportManagerUtils'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; + +// Mini components to render issues + +interface IssueCardProps { + issue: Issue; + onSelect: () => void; +} +export const IssueCard = ({ issue, onSelect }: IssueCardProps) => { + return ( + <div className="issue-card" onClick={onSelect}> + <div className="issue-top"> + <label className="issue-label">#{issue.number}</label> + <div className="issue-tags"> + {issue.labels.map(label => { + const labelString = typeof label === 'string' ? label : label.name ?? ''; + const colors = getLabelColors(labelString); + return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} />; + })} + </div> + </div> + <h3 className="issue-title">{issue.title}</h3> + </div> + ); +}; + +interface IssueViewProps { + issue: Issue; +} + +export const IssueView = ({ issue }: IssueViewProps) => { + const [issueBody, setIssueBody] = React.useState(''); + + const isVideoValid = (src: string) => { + const videoElement = document.createElement('video'); + const validPromise: Promise<boolean> = new Promise(resolve => { + videoElement.addEventListener('loadeddata', () => resolve(true)); + videoElement.addEventListener('error', () => resolve(false)); + }); + videoElement.src = src; + return validPromise; + }; + + const getLinkFromTag = async (tag: string) => { + const regex = /src="([^"]+)"/; + let url = ''; + const match = tag.match(regex); + if (match) { + url = match[1]; + } + + if (url.startsWith('https://github.com/brown-dash/Dash-Web/assets')) { + return `\n${url} (Not authorized to display image here)`; + } + return await getTagFromUrl(url); + }; + + const getTagFromUrl = async (url: string) => { + const imgRegex = /https:\/\/browndash\.com\/files[/\\]images/; + const videoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; + const audioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; + + if (imgRegex.test(url) || url.includes('user-images.githubusercontent.com')) { + return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`; + } else if (videoRegex.test(url)) { + const videoValid = await isVideoValid(url); + if (!videoValid) return `\n${url} (This video could not be loaded)\n`; + return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`; + } else if (audioRegex.test(url)) { + return `\n${url}\n<audio src=${url} controls />\n`; + } else { + return url; + } + }; + + const parseBody = async (body: string) => { + const imgTagRegex = /<img\b[^>]*\/?>/; + const fileRegex = /https:\/\/browndash\.com\/files/; + const parts = body.split('\n'); + + const modifiedParts = await Promise.all( + parts.map(async part => { + if (imgTagRegex.test(part)) { + return `\n${await getLinkFromTag(part)}\n`; + } else if (fileRegex.test(part)) { + const tag = await getTagFromUrl(part); + return tag; + } else { + return part; + } + }) + ); + + setIssueBody(modifiedParts.join('\n')); + }; + + React.useEffect(() => { + parseBody((issue.body as string) ?? ''); + }, [issue]); + + return ( + <div className="issue-view"> + <span className="issue-label"> + Issue{' '} + <a className="issue-link" href={issue.html_url} target="_blank"> + #{issue.number} + </a> + </span> + <h2 className="issue-title">{issue.title}</h2> + <div className="issue-date"> + Opened on {new Date(issue.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} {issue.user?.login && `by ${issue.user?.login}`} + </div> + {issue.labels.length > 0 && ( + <div> + <div className="issue-tags"> + {issue.labels.map(label => { + const labelString = typeof label === 'string' ? label : label.name ?? ''; + const colors = getLabelColors(labelString); + return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} fontSize="12px" />; + })} + </div> + </div> + )} + <ReactMarkdown children={issueBody} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> + </div> + ); +}; + +interface TagProps { + text: string; + fontSize?: string; + color?: string; + backgroundColor?: string; + borderColor?: string; + border?: boolean; + onClick?: () => void; +} + +export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColor, onClick }: TagProps) => { + return ( + <div + onClick={onClick ?? (() => {})} + className="report-tag" + style={{ color: color ?? '#ffffff', backgroundColor: backgroundColor ?? '#347bff', cursor: onClick ? 'pointer' : 'auto', fontSize: fontSize ?? '10px', border: border ? '1px solid' : 'none', borderColor: borderColor ?? '#94a3b8' }}> + {text} + </div> + ); +}; diff --git a/src/client/util/reportManagerSchema.ts b/src/client/util/reportManager/reportManagerSchema.ts index 28ce6411d..56b9aa32b 100644 --- a/src/client/util/reportManagerSchema.ts +++ b/src/client/util/reportManager/reportManagerSchema.ts @@ -1,5 +1,5 @@ /** - * Issues are a great way to keep track of tasks, enhancements, and bugs for your projects. + * Issue interface schema. */ export interface Issue { active_lock_reason?: null | string; diff --git a/src/client/util/reportManager/reportManagerUtils.ts b/src/client/util/reportManager/reportManagerUtils.ts new file mode 100644 index 000000000..3c919856a --- /dev/null +++ b/src/client/util/reportManager/reportManagerUtils.ts @@ -0,0 +1,54 @@ +// Final file url reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" /> + +export enum ViewState { + VIEW, + CREATE, +} + +export enum Priority { + HIGH = 'priority-high', + MEDIUM = 'priority-medium', + LOW = 'priority-low', +} + +export enum BugType { + BUG = 'bug', + COSMETIC = 'cosmetic', + DOCUMENTATION = 'documentation', + ENHANCEMENT = 'enhancement', +} + +// [bgColor, color] +export const priorityColors: { [key: string]: string[] } = { + 'priority-low': ['#d4e0ff', '#000000'], + 'priority-medium': ['#6a91f6', '#ffffff'], + 'priority-high': ['#4476f7', '#ffffff'], +}; + +// [bgColor, color] +export const bugColors: { [key: string]: string[] } = { + bug: ['#fe6d6d', '#ffffff'], + cosmetic: ['#c650f4', '#ffffff'], + documentation: ['#36acf0', '#ffffff'], + 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]; + } else if (bugSet.has(label as BugType)) { + return bugColors[label]; + } + return ['#347bff', '#ffffff']; +}; + +export interface FileData { + _id: string; + file: File; +} + +export const inactiveBorderColor = '#cbd5e1'; +export const inactiveColor = '#64748b'; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 4557e5866..786154620 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -21,7 +21,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; -import { ReportManager } from '../util/ReportManager'; +import { ReportManager } from '../util/reportManager/ReportManager'; import { RTFMarkup } from '../util/RTFMarkup'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SelectionManager } from '../util/SelectionManager'; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index f1d98d22a..fb8ec93b2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -235,6 +235,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo bActive, textX, textY, + // pt1, + // pt2, + // this code adds space between links pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13], }; diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index 4c3b8dabe..d4a7e18f2 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -12,6 +12,10 @@ text-overflow: ellipsis; max-width: 80%; text-align: center; + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; } } diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 20cf563c1..f131032b5 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -11,7 +11,7 @@ import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; import { DocumentManager } from '../../util/DocumentManager'; import { PingManager } from '../../util/PingManager'; -import { ReportManager } from '../../util/ReportManager'; +import { ReportManager } from '../../util/reportManager/ReportManager'; import { ServerStats } from '../../util/ServerStats'; import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; |