change(ui): add steps, change store and some layout
This commit is contained in:
parent
61829d8260
commit
f4350ee083
9 changed files with 261 additions and 38 deletions
|
|
@ -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<string,any>[]): 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 (
|
||||
<div
|
||||
className="flex flex-col p-4 gap-4 bg-white"
|
||||
className="flex flex-col p-4 gap-4 bg-white overflow-y-scroll"
|
||||
style={{ maxWidth: '70vw', width: 620, height: '100vh' }}
|
||||
>
|
||||
<Title userName={account.name} />
|
||||
<MetaInfo envObject={envObject} metadata={metadata} />
|
||||
<Session user={userDisplayName} sessionId={sessionId} />
|
||||
<Steps />
|
||||
<Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} />
|
||||
<Comments />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue