diff --git a/frontend/app/components/Session_/BugReport/BugReportModal.tsx b/frontend/app/components/Session_/BugReport/BugReportModal.tsx index 18f01cb11..1a84b33c1 100644 --- a/frontend/app/components/Session_/BugReport/BugReportModal.tsx +++ b/frontend/app/components/Session_/BugReport/BugReportModal.tsx @@ -1,9 +1,14 @@ import React from 'react'; import { connect } from 'react-redux'; import { countries } from 'App/constants'; +import { useStore } from 'App/mstore'; +import { session as sessionRoute } from 'App/routes'; +import { ReportDefaults, EnvData, Step } from './types' import Session from './components/Session' import MetaInfo from './components/MetaInfo' import Title from './components/Title' +import Comments from './components/Comments' +import Steps from './components/Steps' interface Props { hideModal: () => void; @@ -13,7 +18,48 @@ interface Props { height: number; } +const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' } + +function mapEvents(events: Record[]): Step[] { + const steps: Step[] = [] + events.forEach(event => { + if (event.type === TYPES.LOCATION) { + const step = { + key: event.key, + type: TYPES.LOCATION, + icon: 'pointer', + details: event.url, + time: event.time, + } + steps.push(step) + } + if (event.type === TYPES.CLICK) { + const step = { + key: event.key, + type: TYPES.CLICK, + icon: 'finger', + details: event.label, + time: event.time, + } + steps.push(step) + } + if (event.type === TYPES.CLICKRAGE) { + const step = { + key: event.key, + type: TYPES.CLICKRAGE, + icon: 'smile', + details: event.label, + time: event.time, + } + steps.push(step) + } + }) + + return steps +} + function BugReportModal({ hideModal, session, width, height, account }: Props) { + const { bugReportStore } = useStore() const { userBrowser, userDevice, @@ -26,11 +72,12 @@ function BugReportModal({ hideModal, session, width, height, account }: Props) { revId, metadata, sessionId, + events, } = session; - console.log(session.toJS(), account.toJS?.()) + console.log(session.toJS()) - const envObject = { + const envObject: EnvData = { Device: `${userDevice}${userDeviceType !== userDevice ? ` ${userDeviceType}` : ''}`, Resolution: `${width}x${height}`, Browser: `${userBrowser} v${userBrowserVersion}`, @@ -41,14 +88,30 @@ function BugReportModal({ hideModal, session, width, height, account }: Props) { if (revId) { Object.assign(envObject, { Rev: revId }) } + const sessionUrl = `${window.location.origin}/${window.location.pathname.split('/')[1]}${sessionRoute(sessionId)}` + const defaults: ReportDefaults = { + author: account.name, + env: envObject, + meta: metadata, + session: { + user: userDisplayName, + id: sessionId, + url: sessionUrl, + } + } + + bugReportStore.updateReportDefaults(defaults) + bugReportStore.setSteps(mapEvents(events)) return (
<MetaInfo envObject={envObject} metadata={metadata} /> - <Session user={userDisplayName} sessionId={sessionId} /> + <Steps /> + <Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} /> + <Comments /> </div> ); } diff --git a/frontend/app/components/Session_/BugReport/components/Comments.tsx b/frontend/app/components/Session_/BugReport/components/Comments.tsx new file mode 100644 index 000000000..4ead80d75 --- /dev/null +++ b/frontend/app/components/Session_/BugReport/components/Comments.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import cn from 'classnames'; +import SectionTitle from './SectionTitle'; + +function Comments() { + const { bugReportStore } = useStore(); + const inputRef = React.createRef<HTMLTextAreaElement>(); + + const toggleEdit = () => { + bugReportStore.toggleCommentEditing(true); + }; + + React.useEffect(() => { + if (inputRef.current && bugReportStore.isTitleEdit) { + inputRef.current?.focus(); + } + }, [bugReportStore.isCommentEdit]); + + const commentsEnabled = bugReportStore.comment.length > 0; + const commentStr = commentsEnabled + ? bugReportStore.comment + : 'Expected results, additional steps or any other useful information for debugging.'; + + return ( + <div className="w-full"> + <div className="flex items-center gap-2"> + <SectionTitle>Comments</SectionTitle>{' '} + <div className="text-disabled-text mb-2">(Optional)</div> + </div> + {bugReportStore.isCommentEdit ? ( + <textarea + ref={inputRef} + name="name" + placeholder="Comment..." + rows={3} + autoFocus + className="rounded fluid border -mx-2 px-2 py-1 w-full" + value={bugReportStore.comment} + onChange={(e) => bugReportStore.setComment(e.target.value)} + onBlur={() => bugReportStore.toggleCommentEditing(false)} + onFocus={() => bugReportStore.toggleCommentEditing(true)} + /> + ) : ( + <div + onClick={toggleEdit} + className={cn( + !commentsEnabled + ? 'text-disabled-text border-dotted border-gray-medium' + : 'border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium', + 'h-8 pt-1 flex items-center w-fit', + 'cursor-pointer select-none border-b' + )} + > + {commentStr} + </div> + )} + </div> + ); +} + +export default observer(Comments); diff --git a/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx b/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx index 4c630e039..f8692d095 100644 --- a/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx +++ b/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx @@ -16,7 +16,7 @@ export default function MetaInfo({ envObject, metadata }: { envObject: EnvObj, m <div className="flex flex-col gap-2"> <SectionTitle>Environment</SectionTitle> {Object.keys(envObject).map((envTag) => ( - <div className="flex items-center"> + <div key={envTag} className="flex items-center"> <div className="py-1 px-2">{envTag}</div> <div className="py-1 px-2 text-gray-medium bg-light-blue-bg rounded"> {/* @ts-ignore */} @@ -29,7 +29,7 @@ export default function MetaInfo({ envObject, metadata }: { envObject: EnvObj, m <div className="flex flex-col gap-2"> <SectionTitle>Metadata</SectionTitle> {Object.keys(metadata).map((meta) => ( - <div className="flex items-center rounded overflow-hidden bg-gray-lightest"> + <div key={meta} className="flex items-center rounded overflow-hidden bg-gray-lightest"> <div className="bg-gray-light-shade py-1 px-2">{meta}</div> <div className="py-1 px-2 text-gray-medium">{metadata[meta]}</div> </div> diff --git a/frontend/app/components/Session_/BugReport/components/Session.tsx b/frontend/app/components/Session_/BugReport/components/Session.tsx index 8d318a2fa..160a2e1f4 100644 --- a/frontend/app/components/Session_/BugReport/components/Session.tsx +++ b/frontend/app/components/Session_/BugReport/components/Session.tsx @@ -3,15 +3,15 @@ import SectionTitle from './SectionTitle'; import { session as sessionRoute } from 'App/routes'; import PlayLink from 'Shared/SessionItem/PlayLink'; -export default function Session({ user, sessionId }: { user: string, sessionId: string }) { +export default function Session({ user, sessionId, sessionUrl }: { user: string, sessionId: string, sessionUrl: string }) { return ( <div> <SectionTitle>Session recording</SectionTitle> - <div className="border rounded flex items-center justify-between p-2"> + <div className="border hover:border-gray-light rounded flex items-center justify-between p-2"> <div className="flex flex-col"> <div className="text-lg">{user}</div> <div className="text-disabled-text"> - {`${window.location.origin}/${window.location.pathname.split('/')[1]}${sessionRoute(sessionId)}`} + {sessionUrl} </div> </div> <PlayLink isAssist={false} viewed={false} sessionId={sessionId} /> diff --git a/frontend/app/components/Session_/BugReport/components/Steps.tsx b/frontend/app/components/Session_/BugReport/components/Steps.tsx new file mode 100644 index 000000000..c93fb727d --- /dev/null +++ b/frontend/app/components/Session_/BugReport/components/Steps.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import SectionTitle from './SectionTitle'; +import { Step as IStep } from '../types'; + +const STEP_NAMES = { CLICKRAGE: 'Multiple click', CLICK: 'Clicked', LOCATION: 'Visited' }; + +function Steps() { + const { bugReportStore } = useStore(); + + return ( + <div> + <SectionTitle>Steps to reproduce</SectionTitle> + + <div className="mb-2 text-gray-medium">STEPS</div> + + <div className="flex flex-col gap-4"> + {bugReportStore.sessionEventSteps.map((step, ind) => ( + <React.Fragment key={step.key}> + <Step step={step} ind={ind} /> + </React.Fragment> + ))} + </div> + </div> + ); +} + +function Step({ step, ind }: { step: IStep; ind: number }) { + return ( + <div className="py-1 px-2 flex items-center gap-2 w-full rounded hover:bg-figmaColors-secondary-outlined-hover-background"> + <div className="rounded-3xl px-4 bg-gray-lightest">{ind + 1}</div> + <div className="flex items-center gap-1"> + <div>{step.icon}</div> + {/* @ts-ignore */} + <div className="font-semibold">{STEP_NAMES[step.type]}</div> + <div className="text-gray-medium">{step.details}</div> + </div> + </div> + ); +} + +export default observer(Steps); diff --git a/frontend/app/components/Session_/BugReport/components/Title.tsx b/frontend/app/components/Session_/BugReport/components/Title.tsx index a35ccd208..e756ce0ea 100644 --- a/frontend/app/components/Session_/BugReport/components/Title.tsx +++ b/frontend/app/components/Session_/BugReport/components/Title.tsx @@ -1,17 +1,27 @@ import React from 'react' +import Select from 'Shared/Select'; import ReportTitle from './ReportTitle'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import { SeverityLevels } from 'App/mstore/bugReportStore' + +const selectOptions = [{ label: 'HIGH', value: SeverityLevels.High }, { label: 'MEDIUM', value: SeverityLevels.Medium }, { label: 'LOW', value: SeverityLevels.Low}] + +function Title({ userName }: { userName: string }) { + const { bugReportStore } = useStore(); -export default function Title({ userName }: { userName: string }) { return ( <div className="flex items-center py-2 px-3 justify-between bg-gray-lightest rounded"> <div className="flex flex-col gap-2"> <ReportTitle /> <div className="text-gray-medium">By {userName}</div> </div> - <div> - <div>Severity</div> - <div>select here</div> + <div className="flex items-center gap-2"> + <div className="font-semibold">Severity</div> + <Select plain controlStyle={{ minWidth: 100 }} defaultValue={SeverityLevels.High} options={selectOptions} onChange={({ value }) => bugReportStore.setSeverity(value.value) } /> </div> </div> ); } + +export default observer(Title) diff --git a/frontend/app/components/Session_/BugReport/types.ts b/frontend/app/components/Session_/BugReport/types.ts index 3b514eb05..dbf37bf7e 100644 --- a/frontend/app/components/Session_/BugReport/types.ts +++ b/frontend/app/components/Session_/BugReport/types.ts @@ -1,4 +1,18 @@ -export interface BugReportPdf { +import { SeverityLevels } from 'App/mstore/bugReportStore'; + +export interface BugReportPdf extends ReportDefaults { + title: string; + comment?: string; + severity: SeverityLevels; + steps: Step[]; + activity: { + network: NetworkError[]; + console: ConsoleError[]; + clickRage: ClickRage[]; + }; +} + +export interface ReportDefaults { author: string; env: EnvData; meta: { @@ -9,21 +23,14 @@ export interface BugReportPdf { url: string; id: string; }; - comment?: string; - steps: Step[]; - activity: { - network: NetworkError[]; - console: ConsoleError[]; - clickRage: ClickRage[]; - }; } export interface EnvData { - browser: string; - os: string; - country: string; - device: string; - resolution: string; + Browser: string; + OS: string; + Country: string; + Device: string; + Resolution: string; } export interface NetworkError { @@ -38,15 +45,17 @@ export interface ClickRage { time: number; } -export interface Step { - type: string; - icon: string; - details: string; - substeps?: SubStep[]; -} - export type SubStep = Note | Error | Request; +export interface Step { + key: string; + type: string; + time: number; + details: string; + icon: string; + substeps?: SubStep[] +} + export interface Note { author: string; message: string; diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index 285488e78..b2b7afa8c 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -15,14 +15,15 @@ interface Props<Value extends ValueObject> { defaultValue?: string | number; plain?: boolean; components?: any; - styles?: any; + styles?: Record<string, any>; + controlStyle?: Record<string, any>; onChange: (newValue: { name: string, value: Value }) => void; name?: string; placeholder?: string; [x:string]: any; } -export default function<Value extends ValueObject>({ placeholder='Select', name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', ...rest }: Props<Value>) { +export default function<Value extends ValueObject>({ placeholder='Select', name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', controlStyle = {}, ...rest }: Props<Value>) { const defaultSelected = defaultValue ? (options.find(o => o.value === defaultValue) || options[0]): null; const customStyles = { option: (provided: any, state: any) => ({ @@ -71,7 +72,8 @@ export default function<Value extends ValueObject>({ placeholder='Select', name ['&:hover']: { backgroundColor: colors['gray-lightest'], transition: 'all 0.2s ease-in-out' - } + }, + ...controlStyle, } if (plain) { obj['backgroundColor'] = 'transparent'; diff --git a/frontend/app/mstore/bugReportStore.ts b/frontend/app/mstore/bugReportStore.ts index 9eb6eeb78..9bbab2aaa 100644 --- a/frontend/app/mstore/bugReportStore.ts +++ b/frontend/app/mstore/bugReportStore.ts @@ -1,6 +1,7 @@ import { makeAutoObservable } from "mobx" +import { BugReportPdf, ReportDefaults, Step } from 'Components/Session_/BugReport/types' -enum SeverityLevels { +export enum SeverityLevels { Low, Medium, High @@ -8,9 +9,15 @@ enum SeverityLevels { export default class BugReportStore { reportTitle = 'Untitled Report' - isTitleEdit = false + comment = '' severity = SeverityLevels.High + isCommentEdit = false + isTitleEdit = false + + bugReport: Partial<BugReportPdf> + sessionEventSteps: Step[] = [] + constructor() { makeAutoObservable(this) } @@ -21,5 +28,31 @@ export default class BugReportStore { setTitle(title: string) { this.reportTitle = title + + this.bugReport = Object.assign(this.bugReport, { title: this.reportTitle }) + } + + setSeverity(severity: SeverityLevels) { + this.severity = severity + + this.bugReport = Object.assign(this.bugReport, { severity: this.severity }) + } + + toggleCommentEditing(isEdit: boolean) { + this.isCommentEdit = isEdit + } + + setComment(comment: string) { + this.comment = comment + + this.bugReport = Object.assign(this.bugReport, { comment: this.comment.length > 0 ? this.comment : undefined }) + } + + updateReportDefaults(defaults: ReportDefaults) { + this.bugReport = Object.assign(this.bugReport || {}, defaults) + } + + setSteps(steps: Step[]) { + this.sessionEventSteps = steps } }