change(ui) - player user steps improvements (#1201)
* change(ui) - user steps * change(ui) - user steps * change(ui) - user steps * change(ui) - user steps - icon and other styles
This commit is contained in:
parent
8b6dff356e
commit
d9404d1d13
10 changed files with 414 additions and 408 deletions
|
|
@ -1,195 +0,0 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis, Tooltip } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { prorata } from 'App/utils';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
import LoadInfo from './LoadInfo';
|
||||
import cls from './event.module.css';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
function isFrustrationEvent(evt) {
|
||||
if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) {
|
||||
return true;
|
||||
}
|
||||
if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) {
|
||||
return evt.hesitation > 1000
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@withOverlay()
|
||||
export default class Event extends React.PureComponent {
|
||||
state = {
|
||||
menuOpen: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.wrapper.addEventListener('contextmenu', this.onContextMenu);
|
||||
}
|
||||
|
||||
onContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ menuOpen: true });
|
||||
}
|
||||
onMouseLeave = () => this.setState({ menuOpen: false })
|
||||
|
||||
copyHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
//const ctrlOrCommandPressed = e.ctrlKey || e.metaKey;
|
||||
//if (ctrlOrCommandPressed && e.keyCode === 67) {
|
||||
const { event } = this.props;
|
||||
copy(event.getIn([ 'target', 'path' ]) || event.url || '');
|
||||
this.setState({ menuOpen: false });
|
||||
}
|
||||
|
||||
toggleInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleInfo();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
renderBody = () => {
|
||||
const { event } = this.props;
|
||||
let title = event.type;
|
||||
let body;
|
||||
let icon;
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
const tooltip = { disabled: true, text: '' }
|
||||
|
||||
switch (event.type) {
|
||||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
icon = 'location';
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
icon = isFrustration ? 'click_hesitation' : 'click';
|
||||
isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to click for ${Math.round(event.hesitation/1000)}s`, }) : null;
|
||||
break;
|
||||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
icon = isFrustration ? 'input_hesitation' : 'input';
|
||||
isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to enter a value for ${Math.round(event.hesitation/1000)}s`, }) : null;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${ event.count } Clicks`;
|
||||
body = event.label;
|
||||
icon = 'clickrage'
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
icon = 'ios_view'
|
||||
break;
|
||||
case 'mouse_thrashing':
|
||||
title = 'Mouse Thrashing';
|
||||
icon = 'mouse_thrashing'
|
||||
break;
|
||||
}
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip.text} disabled={tooltip.disabled} placement={"left"} anchorClassName={"w-full"} containerClassName={"w-full"}>
|
||||
<div className={ cn(cls.main, 'flex flex-col w-full') } >
|
||||
<div className="flex items-center w-full">
|
||||
{ event.type && <Icon name={`event/${icon}`} size="16" color={'gray-dark' } /> }
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex items-center w-full" style={{ minWidth: '0'}}>
|
||||
<span className={cls.title}>{ title }</span>
|
||||
{/* { body && !isLocation && <div className={ cls.data }>{ body }</div> } */}
|
||||
{ body && !isLocation &&
|
||||
<TextEllipsis maxWidth="60%" className="w-full ml-2 text-sm color-gray-medium" text={body} />
|
||||
}
|
||||
</div>
|
||||
{ isLocation && event.speedIndex != null &&
|
||||
<div className="color-gray-medium flex font-medium items-center leading-none justify-end">
|
||||
<div className="font-size-10 pr-2">{"Speed Index"}</div>
|
||||
<div>{ numberWithCommas(event.speedIndex || 0) }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{ event.target && event.target.label &&
|
||||
<div className={ cls.badge } >{ event.target.label }</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{ isLocation &&
|
||||
<div className="mt-1">
|
||||
<span className="text-sm font-normal color-gray-medium">{ body }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
selected,
|
||||
isCurrent,
|
||||
onClick,
|
||||
showSelection,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed,
|
||||
presentInSearch = false,
|
||||
whiteBg,
|
||||
} = this.props;
|
||||
const { menuOpen } = this.state;
|
||||
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
return (
|
||||
<div
|
||||
ref={ ref => { this.wrapper = ref } }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
data-openreplay-label="Event"
|
||||
data-type={event.type}
|
||||
className={ cn(cls.event, {
|
||||
[ cls.menuClosed ]: !menuOpen,
|
||||
[ cls.highlighted ]: showSelection ? selected : isCurrent,
|
||||
[ cls.selected ]: selected,
|
||||
[ cls.showSelection ]: showSelection,
|
||||
[ cls.red ]: isRed,
|
||||
[ cls.clickType ]: event.type === TYPES.CLICK,
|
||||
[ cls.inputType ]: event.type === TYPES.INPUT,
|
||||
[ cls.frustration ]: isFrustration,
|
||||
[ cls.highlight ] : presentInSearch,
|
||||
[ cls.lastInGroup ]: whiteBg,
|
||||
}) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ menuOpen &&
|
||||
<button onClick={ this.copyHandler } className={ cls.contextMenu }>
|
||||
{ event.target ? 'Copy CSS' : 'Copy URL' }
|
||||
</button>
|
||||
}
|
||||
<div className={ cn(cls.topBlock, 'w-full') }>
|
||||
<div className={ cn(cls.firstLine, 'w-full') }>
|
||||
{ this.renderBody() }
|
||||
</div>
|
||||
</div>
|
||||
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) &&
|
||||
<LoadInfo
|
||||
showInfo={ showLoadInfo }
|
||||
onClick={ toggleLoadInfo }
|
||||
event={ event }
|
||||
prorata={ prorata({
|
||||
parts: 100,
|
||||
elements: { a: event.fcpTime, b: event.visuallyComplete, c: event.timeToInteractive },
|
||||
startDivisorFn: elements => elements / 1.2,
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
}) }
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
215
frontend/app/components/Session_/EventsBlock/Event.tsx
Normal file
215
frontend/app/components/Session_/EventsBlock/Event.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis, Tooltip } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { prorata } from 'App/utils';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
import LoadInfo from './LoadInfo';
|
||||
import cls from './event.module.css';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
type Props = {
|
||||
event: any;
|
||||
selected?: boolean;
|
||||
isCurrent?: boolean;
|
||||
onClick?: () => void;
|
||||
showSelection?: boolean;
|
||||
showLoadInfo?: boolean;
|
||||
toggleLoadInfo?: () => void;
|
||||
isRed?: boolean;
|
||||
presentInSearch?: boolean;
|
||||
whiteBg?: boolean;
|
||||
};
|
||||
|
||||
const isFrustrationEvent = (evt: any): boolean => {
|
||||
if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) {
|
||||
return true;
|
||||
}
|
||||
if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) {
|
||||
return evt.hesitation > 1000;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const Event: React.FC<Props> = ({
|
||||
event,
|
||||
selected = false,
|
||||
isCurrent = false,
|
||||
onClick,
|
||||
showSelection = false,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed = false,
|
||||
presentInSearch = false,
|
||||
whiteBg,
|
||||
}) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setMenuOpen(true);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => setMenuOpen(false);
|
||||
|
||||
const copyHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
const path = event.getIn(['target', 'path']) || event.url || '';
|
||||
copy(path);
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
const renderBody = () => {
|
||||
let title = event.type;
|
||||
let body;
|
||||
let icon;
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
const tooltip = { disabled: true, text: '' };
|
||||
|
||||
switch (event.type) {
|
||||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
icon = 'location';
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
icon = isFrustration ? 'click_hesitation' : 'click';
|
||||
isFrustration
|
||||
? Object.assign(tooltip, {
|
||||
disabled: false,
|
||||
text: `User hesitated to click for ${Math.round(event.hesitation / 1000)}s`,
|
||||
})
|
||||
: null;
|
||||
break;
|
||||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
icon = isFrustration ? 'input_hesitation' : 'input';
|
||||
isFrustration
|
||||
? Object.assign(tooltip, {
|
||||
disabled: false,
|
||||
text: `User hesitated to enter a value for ${Math.round(event.hesitation / 1000)}s`,
|
||||
})
|
||||
: null;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${event.count} Clicks`;
|
||||
body = event.label;
|
||||
icon = 'clickrage';
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
icon = 'ios_view';
|
||||
break;
|
||||
case 'mouse_thrashing':
|
||||
title = 'Mouse Thrashing';
|
||||
icon = 'mouse_thrashing';
|
||||
break;
|
||||
}
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltip.text}
|
||||
disabled={tooltip.disabled}
|
||||
placement={'left'}
|
||||
anchorClassName={'w-full'}
|
||||
containerClassName={'w-full'}
|
||||
>
|
||||
<div className={cn(cls.main, 'flex flex-col w-full')}>
|
||||
<div className={cn("flex items-center w-full", {'px-4' : isLocation })}>
|
||||
{event.type && <Icon name={`event/${icon}`} size="16" color={'gray-dark'} />}
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex items-center w-full" style={{ minWidth: '0' }}>
|
||||
<span className={cn(cls.title, {'font-medium' : isLocation })}>{title}</span>
|
||||
{body && !isLocation && (
|
||||
<TextEllipsis
|
||||
maxWidth="60%"
|
||||
className="w-full ml-2 text-sm color-gray-medium"
|
||||
text={body}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isLocation && event.speedIndex != null && (
|
||||
<div className="color-gray-medium flex font-medium items-center leading-none justify-end">
|
||||
<div className="font-size-10 pr-2">{'Speed Index'}</div>
|
||||
<div>{numberWithCommas(event.speedIndex || 0)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{event.target && event.target.label && (
|
||||
<div className={cls.badge}>{event.target.label}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLocation && (
|
||||
<div className="mt-1 px-4">
|
||||
<span className="text-sm font-normal color-gray-medium">{body}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
onMouseLeave={onMouseLeave}
|
||||
data-openreplay-label="Event"
|
||||
data-type={event.type}
|
||||
className={cn(cls.event, {
|
||||
[cls.menuClosed]: !menuOpen,
|
||||
[cls.highlighted]: showSelection ? selected : isCurrent,
|
||||
[cls.selected]: selected,
|
||||
[cls.showSelection]: showSelection,
|
||||
[cls.red]: isRed,
|
||||
[cls.clickType]: event.type === TYPES.CLICK,
|
||||
[cls.inputType]: event.type === TYPES.INPUT,
|
||||
[cls.frustration]: isFrustration,
|
||||
[cls.highlight]: presentInSearch,
|
||||
[cls.lastInGroup]: whiteBg,
|
||||
['mx-4 rounded']: event.type !== TYPES.LOCATION,
|
||||
})}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{menuOpen && (
|
||||
<button onClick={copyHandler} className={cls.contextMenu}>
|
||||
{event.target ? 'Copy CSS' : 'Copy URL'}
|
||||
</button>
|
||||
)}
|
||||
<div className={cn(cls.topBlock, 'w-full')}>
|
||||
<div className={cn(cls.firstLine, 'w-full')}>{renderBody()}</div>
|
||||
</div>
|
||||
{event.type === TYPES.LOCATION &&
|
||||
(event.fcpTime || event.visuallyComplete || event.timeToInteractive) && (
|
||||
<LoadInfo
|
||||
showInfo={showLoadInfo}
|
||||
onClick={toggleLoadInfo}
|
||||
event={event}
|
||||
prorata={prorata({
|
||||
parts: 100,
|
||||
elements: {
|
||||
a: event.fcpTime,
|
||||
b: event.visuallyComplete,
|
||||
c: event.timeToInteractive,
|
||||
},
|
||||
startDivisorFn: (elements) => elements / 1.2,
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withOverlay()(Event);
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import { TextEllipsis } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import Event from './Event'
|
||||
import Event from './Event';
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import { setEditNoteTooltip } from 'Duck/sessions';;
|
||||
import { setEditNoteTooltip } from 'Duck/sessions';
|
||||
|
||||
// TODO: incapsulate toggler in LocationEvent
|
||||
@withToggle('showLoadInfo', 'toggleLoadInfo')
|
||||
|
|
@ -66,65 +66,68 @@ class EventGroupWrapper extends React.Component {
|
|||
const safeRef = String(event.referrer || '');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
stl.container,
|
||||
'!py-1',
|
||||
{
|
||||
[stl.last]: isLastInGroup,
|
||||
[stl.first]: event.type === TYPES.LOCATION,
|
||||
[stl.dashAfter]: isLastInGroup && !isLastEvent,
|
||||
},
|
||||
isLastInGroup && '!pb-2',
|
||||
event.type === TYPES.LOCATION && '!pt-2 !pb-2'
|
||||
)}
|
||||
>
|
||||
{isFirst && isLocation && event.referrer && (
|
||||
<div className={stl.referrer}>
|
||||
<TextEllipsis>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
</TextEllipsis>
|
||||
</div>
|
||||
)}
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
note={event}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
noEdit={this.props.currentUserId !== event.userId}
|
||||
/>
|
||||
) : isLocation ? (
|
||||
<Event
|
||||
extended={isFirst}
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
selected={isSelected}
|
||||
showLoadInfo={showLoadInfo}
|
||||
toggleLoadInfo={this.toggleLoadInfo}
|
||||
isCurrent={isCurrent}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
) : (
|
||||
<Event
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
onCheckboxClick={this.onCheckboxClick}
|
||||
selected={isSelected}
|
||||
isCurrent={isCurrent}
|
||||
showSelection={showSelection}
|
||||
overlayed={isEditing}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'!py-1',
|
||||
{
|
||||
[stl.last]: isLastInGroup,
|
||||
[stl.first]: event.type === TYPES.LOCATION,
|
||||
[stl.dashAfter]: isLastInGroup && !isLastEvent,
|
||||
},
|
||||
isLastInGroup && '!pb-2',
|
||||
event.type === TYPES.LOCATION && '!pt-2 !pb-2'
|
||||
)}
|
||||
>
|
||||
{isFirst && isLocation && event.referrer && (
|
||||
|
||||
<TextEllipsis>
|
||||
<div className={stl.referrer}>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
</div>
|
||||
</TextEllipsis>
|
||||
)}
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
note={event}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
noEdit={this.props.currentUserId !== event.userId}
|
||||
/>
|
||||
) : isLocation ? (
|
||||
<Event
|
||||
extended={isFirst}
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
selected={isSelected}
|
||||
showLoadInfo={showLoadInfo}
|
||||
toggleLoadInfo={this.toggleLoadInfo}
|
||||
isCurrent={isCurrent}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={true}
|
||||
/>
|
||||
) : (
|
||||
<Event
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
onCheckboxClick={this.onCheckboxClick}
|
||||
selected={isSelected}
|
||||
isCurrent={isCurrent}
|
||||
showSelection={showSelection}
|
||||
overlayed={isEditing}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isLastInGroup && <div className="border-t mx-5 border-color-gray-light-shade" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EventGroupWrapper
|
||||
export default EventGroupWrapper;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,44 @@
|
|||
import React from 'react'
|
||||
import { Input, Icon } from 'UI'
|
||||
import React from 'react';
|
||||
import { Input, Button } from 'UI';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
||||
function EventSearch(props) {
|
||||
const { player } = React.useContext(PlayerContext)
|
||||
const { player } = React.useContext(PlayerContext);
|
||||
|
||||
const { onChange, value, header, setActiveTab } = props;
|
||||
|
||||
const toggleEvents = () => player.toggleEvents()
|
||||
const toggleEvents = () => player.toggleEvents();
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full relative">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className='flex flex-center justify-between'>
|
||||
<span>{header}</span>
|
||||
<div
|
||||
onClick={() => { setActiveTab(''); toggleEvents(); }}
|
||||
className=" flex items-center justify-center bg-white cursor-pointer"
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Filter by Event Type, URL or Keyword"
|
||||
className="inset-0 w-full"
|
||||
name="query"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
wrapperClassName="w-full"
|
||||
style={{ height: '32px' }}
|
||||
autoComplete="off chromebugfix"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="ml-2"
|
||||
icon="close"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
toggleEvents();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Filter by Event Type, URL or Keyword"
|
||||
className="inset-0 w-full"
|
||||
name="query"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
wrapperClassName="w-full"
|
||||
style={{ height: '32px' }}
|
||||
autoComplete="off chromebugfix"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default EventSearch
|
||||
export default EventSearch;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
|
||||
import { List, AutoSizer, CellMeasurer } from 'react-virtualized';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setEventFilter, filterOutNote } from 'Duck/sessions';
|
||||
import EventGroupWrapper from './EventGroupWrapper';
|
||||
|
|
@ -10,33 +10,33 @@ import styles from './eventsBlock.module.css';
|
|||
import EventSearch from './EventSearch/EventSearch';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { RootStore } from 'App/duck'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { InjectedEvent } from 'Types/session/event'
|
||||
import Session from 'Types/session'
|
||||
import { RootStore } from 'App/duck';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
import { InjectedEvent } from 'Types/session/event';
|
||||
import Session from 'Types/session';
|
||||
|
||||
interface IProps {
|
||||
setEventFilter: (filter: { query: string }) => void
|
||||
filteredEvents: InjectedEvent[]
|
||||
setActiveTab: (tab?: string) => void
|
||||
query: string
|
||||
events: Session['events']
|
||||
notesWithEvents: Session['notesWithEvents']
|
||||
filterOutNote: (id: string) => void
|
||||
eventsIndex: number[]
|
||||
setEventFilter: (filter: { query: string }) => void;
|
||||
filteredEvents: InjectedEvent[];
|
||||
setActiveTab: (tab?: string) => void;
|
||||
query: string;
|
||||
events: Session['events'];
|
||||
notesWithEvents: Session['notesWithEvents'];
|
||||
filterOutNote: (id: string) => void;
|
||||
eventsIndex: number[];
|
||||
}
|
||||
|
||||
function EventsBlock(props: IProps) {
|
||||
const [mouseOver, setMouseOver] = React.useState(true)
|
||||
const scroller = React.useRef<List>(null)
|
||||
const [mouseOver, setMouseOver] = React.useState(true);
|
||||
const scroller = React.useRef<List>(null);
|
||||
const cache = useCellMeasurerCache(undefined, {
|
||||
fixedWidth: true,
|
||||
defaultHeight: 300
|
||||
defaultHeight: 300,
|
||||
});
|
||||
|
||||
const { store, player } = React.useContext(PlayerContext)
|
||||
const { store, player } = React.useContext(PlayerContext);
|
||||
|
||||
const { eventListNow, playing } = store.get()
|
||||
const { eventListNow, playing } = store.get();
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
|
|
@ -46,23 +46,23 @@ function EventsBlock(props: IProps) {
|
|||
setActiveTab,
|
||||
events,
|
||||
notesWithEvents,
|
||||
} = props
|
||||
} = props;
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
|
||||
const usedEvents = filteredEvents || notesWithEvents
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0;
|
||||
const usedEvents = filteredEvents || notesWithEvents;
|
||||
|
||||
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.setEventFilter({ query: value })
|
||||
props.setEventFilter({ query: value });
|
||||
|
||||
setTimeout(() => {
|
||||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
props.setEventFilter({ query: '' })
|
||||
props.setEventFilter({ query: '' });
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
}
|
||||
|
|
@ -71,14 +71,14 @@ function EventsBlock(props: IProps) {
|
|||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearSearch()
|
||||
}
|
||||
}, [])
|
||||
clearSearch();
|
||||
};
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
|
|
@ -86,40 +86,49 @@ function EventsBlock(props: IProps) {
|
|||
scroller.current.scrollToRow(currentTimeEventIndex);
|
||||
}
|
||||
}
|
||||
}, [currentTimeEventIndex])
|
||||
}, [currentTimeEventIndex]);
|
||||
|
||||
const onEventClick = (_: React.MouseEvent, event: { time: number }) => player.jump(event.time)
|
||||
const onMouseOver = () => setMouseOver(true)
|
||||
const onMouseLeave = () => setMouseOver(false)
|
||||
const onEventClick = (_: React.MouseEvent, event: { time: number }) => {
|
||||
player.jump(event.time);
|
||||
props.setEventFilter({ query: '' });
|
||||
};
|
||||
const onMouseOver = () => setMouseOver(true);
|
||||
const onMouseLeave = () => setMouseOver(false);
|
||||
|
||||
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: React.CSSProperties; parent: any }) => {
|
||||
const renderGroup = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
parent,
|
||||
}: {
|
||||
index: number;
|
||||
key: string;
|
||||
style: React.CSSProperties;
|
||||
parent: any;
|
||||
}) => {
|
||||
const isLastEvent = index === usedEvents.length - 1;
|
||||
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
|
||||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event
|
||||
const isNote = 'noteId' in event;
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
|
||||
const heightBug = index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {}
|
||||
const heightBug =
|
||||
index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {};
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
{({measure, registerChild}) => (
|
||||
<CellMeasurer key={key} cache={cache} parent={parent} rowIndex={index}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div style={{ ...style, ...heightBug }} ref={registerChild}>
|
||||
<EventGroupWrapper
|
||||
query={query}
|
||||
presentInSearch={eventsIndex.includes(index)}
|
||||
isFirst={index==0}
|
||||
isFirst={index == 0}
|
||||
mesureHeight={measure}
|
||||
onEventClick={ onEventClick }
|
||||
event={ event }
|
||||
isLastEvent={ isLastEvent }
|
||||
isLastInGroup={ isLastInGroup }
|
||||
isCurrent={ isCurrent }
|
||||
showSelection={ !playing }
|
||||
onEventClick={onEventClick}
|
||||
event={event}
|
||||
isLastEvent={isLastEvent}
|
||||
isLastInGroup={isLastInGroup}
|
||||
isCurrent={isCurrent}
|
||||
showSelection={!playing}
|
||||
isNote={isNote}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
|
|
@ -127,50 +136,44 @@ function EventsBlock(props: IProps) {
|
|||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents)
|
||||
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents);
|
||||
return (
|
||||
<>
|
||||
<div className={ cn(styles.header, 'p-4') }>
|
||||
<div className={ cn(styles.hAndProgress, 'mt-3') }>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
header={
|
||||
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.length }</span></div>
|
||||
}
|
||||
/>
|
||||
<div className={cn(styles.header, 'p-4')}>
|
||||
<div className={cn(styles.hAndProgress, 'mt-3')}>
|
||||
<EventSearch onChange={write} setActiveTab={setActiveTab} value={query} />
|
||||
</div>
|
||||
<div className="mt-1 color-gray-medium">Displaying {usedEvents.length} events</div>
|
||||
</div>
|
||||
<div
|
||||
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
|
||||
className={cn('flex-1 pb-4', styles.eventsList)}
|
||||
id="eventList"
|
||||
data-openreplay-masked
|
||||
onMouseOver={ onMouseOver }
|
||||
onMouseLeave={ onMouseLeave }
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{isEmptySearch && (
|
||||
<div className='flex items-center'>
|
||||
<div className="flex items-center p-4">
|
||||
<Icon name="binoculars" size={18} />
|
||||
<span className='ml-2'>No Matching Results</span>
|
||||
<span className="ml-2">No Matching Results</span>
|
||||
</div>
|
||||
)}
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
ref={scroller}
|
||||
className={ styles.eventsList }
|
||||
className={styles.eventsList}
|
||||
height={height + 10}
|
||||
width={248}
|
||||
width={270}
|
||||
overscanRowCount={6}
|
||||
itemSize={230}
|
||||
rowCount={usedEvents.length}
|
||||
deferredMeasurementCache={cache}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={renderGroup}
|
||||
scrollToAlignment="start"
|
||||
scrollToAlignment="center"
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
|
@ -179,14 +182,17 @@ function EventsBlock(props: IProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect((state: RootStore) => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
notesWithEvents: state.getIn([ 'sessions', 'current' ]).notesWithEvents,
|
||||
events: state.getIn([ 'sessions', 'current' ]).events,
|
||||
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
|
||||
}), {
|
||||
setEventFilter,
|
||||
filterOutNote
|
||||
})(observer(EventsBlock))
|
||||
export default connect(
|
||||
(state: RootStore) => ({
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
notesWithEvents: state.getIn(['sessions', 'current']).notesWithEvents,
|
||||
events: state.getIn(['sessions', 'current']).events,
|
||||
filteredEvents: state.getIn(['sessions', 'filteredEvents']),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn(['sessions', 'eventsIndex']),
|
||||
}),
|
||||
{
|
||||
setEventFilter,
|
||||
filterOutNote,
|
||||
}
|
||||
)(observer(EventsBlock));
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
position: absolute;
|
||||
top: 27px;
|
||||
right: 15px;
|
||||
padding: 2px 3px;
|
||||
/* padding: 2px 3px; */
|
||||
background: $white;
|
||||
border: 1px solid $gray-light;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: $gray-medium;
|
||||
|
|
@ -15,15 +14,12 @@
|
|||
.event {
|
||||
position: relative;
|
||||
background: #f6f6f6;
|
||||
border-radius: 3px;
|
||||
/* border-radius: 3px; */
|
||||
user-select: none;
|
||||
/* box-shadow: 0px 1px 3px 0 $gray-light; */
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
border: 1px solid $active-blue-border;
|
||||
}
|
||||
|
||||
& .title {
|
||||
|
|
@ -55,7 +51,7 @@
|
|||
|
||||
&.menuClosed.showSelection {
|
||||
&:hover, &.selected {
|
||||
background-color: #EFFCFB;
|
||||
background-color: $active-blue;
|
||||
|
||||
& .checkbox {
|
||||
display: flex;
|
||||
|
|
@ -69,9 +65,8 @@
|
|||
|
||||
&.highlighted {
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0px 2px 10px 0 $gray-light;
|
||||
border: 1px solid $active-blue-border;
|
||||
/* background-color: red; */
|
||||
background-color: $active-blue;
|
||||
box-shadow: 0 0 0 2px $active-blue;
|
||||
}
|
||||
|
||||
&.red {
|
||||
|
|
@ -136,37 +131,22 @@
|
|||
|
||||
|
||||
.clickType, .inputType {
|
||||
/* border: 1px solid $gray-light; */
|
||||
background-color: $gray-lightest;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.frustration {
|
||||
background-color: rgba(204, 0, 0, 0.1)!important;
|
||||
box-shadow:
|
||||
2px 2px 1px 1px white,
|
||||
2px 2px 0px 1px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.clickrageType {
|
||||
background-color: #FFF3F3;
|
||||
border: 1px solid #CC0000;
|
||||
box-shadow:
|
||||
/* The top layer shadow */
|
||||
/* 0 1px 1px rgba(0,0,0,0.15), */
|
||||
/* The second layer */
|
||||
2px 2px 1px 1px white,
|
||||
/* The second layer shadow */
|
||||
2px 2px 0px 1px rgba(0,0,0,0.4);
|
||||
/* Padding for demo purposes */
|
||||
/* padding: 12px; */
|
||||
}
|
||||
|
||||
.highlight {
|
||||
border: solid thin red;
|
||||
/* border: solid thin red; */
|
||||
}
|
||||
|
||||
.lastInGroup {
|
||||
background: white;
|
||||
box-shadow: 0px 1px 1px 0px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
.container {
|
||||
padding: 0px 7px; /*0.35rem 0.5rem */
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.first {
|
||||
padding-top: 7px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.last {
|
||||
padding-bottom: 7px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: 5px;
|
||||
/* border-bottom: 1px solid $gray-lightest; */
|
||||
}
|
||||
|
||||
.dashAfter {
|
||||
|
|
@ -25,6 +19,9 @@
|
|||
font-weight: 500 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 280px;
|
||||
margin: 0 20px;
|
||||
margin-bottom: 5px;
|
||||
& .url {
|
||||
margin-left: 5px;
|
||||
font-weight: 300;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.eventsBlock {
|
||||
width: 270px;
|
||||
/* width: 290px; */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-cursor" viewBox="0 0 16 16">
|
||||
<path d="M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103zM2.25 8.184l3.897 1.67a.5.5 0 0 1 .262.263l1.67 3.897L12.743 3.52 2.25 8.184z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-send" viewBox="0 0 16 16">
|
||||
<path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576 6.636 10.07Zm6.787-8.201L1.591 6.602l4.339 2.76 7.494-7.493Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 361 B |
Loading…
Add table
Reference in a new issue