feat(ui): draft note taking popup
This commit is contained in:
parent
0c79993b65
commit
fdc9b1d305
12 changed files with 269 additions and 100 deletions
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { Duration } from 'luxon';
|
||||
import { connect } from 'react-redux';
|
||||
// @ts-ignore
|
||||
import stl from './timeline.module.css';
|
||||
|
||||
function TimeTooltip({ time, offset, isVisible, liveTimeTravel }: { time: number; offset: number; isVisible: boolean, liveTimeTravel: boolean }) {
|
||||
const duration = Duration.fromMillis(time).toFormat(`${liveTimeTravel ? '-' : ''}mm:ss`);
|
||||
return (
|
||||
<div
|
||||
className={stl.timeTooltip}
|
||||
style={{
|
||||
top: -30,
|
||||
left: offset - 20,
|
||||
display: isVisible ? 'block' : 'none' }
|
||||
}
|
||||
>
|
||||
{!time ? 'Loading' : duration}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => {
|
||||
const { time = 0, offset = 0, isVisible } = state.getIn(['sessions', 'timeLineTooltip']);
|
||||
return { time, offset, isVisible };
|
||||
})(TimeTooltip);
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { Icon, Button, Checkbox } from 'UI';
|
||||
import { Duration } from 'luxon';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './styles.module.css';
|
||||
|
||||
interface Props {
|
||||
offset: number;
|
||||
isVisible: boolean;
|
||||
time: number;
|
||||
}
|
||||
|
||||
const TAGS = ['QUERY', 'ISSUE', 'TASK', 'OTHER'];
|
||||
|
||||
function NoteTooltip({ offset, isVisible, time }: Props) {
|
||||
const duration = Duration.fromMillis(time).toFormat('mm:ss');
|
||||
|
||||
const stopEvents = (e: any) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={stl.noteTooltip}
|
||||
style={{
|
||||
top: -250,
|
||||
width: 350,
|
||||
left: offset - 20,
|
||||
display: isVisible ? 'block' : 'none',
|
||||
}}
|
||||
onClick={stopEvents}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon name="quotes" size={20} />
|
||||
<h3 className="text-xl ml-2 mr-4 font-semibold">Add Note</h3>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Checkbox />
|
||||
<span className="ml-1">{`at ${duration}`}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto cursor-pointer">
|
||||
<Icon name="close" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>text field</div>
|
||||
|
||||
<div className="flex items-center mt-4">
|
||||
{TAGS.map((tag) => (
|
||||
<div className="rounded-xl px-2 py-1 mr-2 bg-gray-medium"> {tag} </div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-4">
|
||||
<Button variant="primary" className="mr-4">
|
||||
Add Note
|
||||
</Button>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Checkbox />
|
||||
<Icon name="user-friends" size={16} className="mx-1" />
|
||||
Visible to the team
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={stl.arrow} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => {
|
||||
const { offset = 0, isVisible, time = 0 } = state.getIn(['sessions', 'noteTooltip']);
|
||||
return { offset, isVisible, time };
|
||||
})(NoteTooltip);
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { Duration } from 'luxon';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './styles.module.css';
|
||||
|
||||
interface Props {
|
||||
time: number;
|
||||
offset: number;
|
||||
isVisible: boolean;
|
||||
liveTimeTravel: boolean;
|
||||
}
|
||||
|
||||
function TimeTooltip({
|
||||
time,
|
||||
offset,
|
||||
isVisible,
|
||||
liveTimeTravel,
|
||||
}: Props) {
|
||||
const duration = Duration.fromMillis(time).toFormat(`${liveTimeTravel ? '-' : ''}mm:ss`);
|
||||
return (
|
||||
<div
|
||||
className={stl.timeTooltip}
|
||||
style={{
|
||||
top: -30,
|
||||
left: offset - 20,
|
||||
display: isVisible ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
{!time ? 'Loading' : duration}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => {
|
||||
const { time = 0, offset = 0, isVisible } = state.getIn(['sessions', 'timeLineTooltip']);
|
||||
return { time, offset, isVisible };
|
||||
})(TimeTooltip);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import TimeTooltip from '../TimeTooltip';
|
||||
import TimeTooltip from './TimeTooltip';
|
||||
import NoteTooltip from './NoteTooltip';
|
||||
import store from 'App/store';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
|
|
@ -7,7 +8,10 @@ function TooltipContainer({ live }: { live: boolean }) {
|
|||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<TimeTooltip liveTimeTravel={live} />
|
||||
<>
|
||||
<TimeTooltip liveTimeTravel={live} />
|
||||
<NoteTooltip />
|
||||
</>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
.timeTooltip {
|
||||
position: absolute;
|
||||
padding: 0.25rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
background: black;
|
||||
top: -35px;
|
||||
color: white;
|
||||
|
||||
&:after {
|
||||
content:'';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: solid 5px black;
|
||||
border-left: solid 5px transparent;
|
||||
border-right: solid 5px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.noteTooltip {
|
||||
position: absolute;
|
||||
padding: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
background: #F5F5F5;
|
||||
top: -35px;
|
||||
color: black;
|
||||
border-radius: 12px;
|
||||
cursor: default;
|
||||
box-shadow: 0 4px 20px 4px rgb(0 20 60 / 10%), 0 4px 80px -8px rgb(0 20 60 / 20%);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: solid 10px #f5f5f5;
|
||||
border-left: solid 10px transparent;
|
||||
border-right: solid 10px transparent;
|
||||
}
|
||||
|
|
@ -1,80 +1,91 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import Autoplay from './Autoplay';
|
||||
import Bookmark from 'Shared/Bookmark'
|
||||
import Bookmark from 'Shared/Bookmark';
|
||||
import SharePopup from '../shared/SharePopup/SharePopup';
|
||||
import { connectPlayer } from 'Player';
|
||||
import { connectPlayer, pause } from 'Player';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import Issues from './Issues/Issues';
|
||||
|
||||
function SubHeader(props) {
|
||||
const [isCopied, setCopied] = React.useState(false);
|
||||
const [isCopied, setCopied] = React.useState(false);
|
||||
|
||||
const isAssist = window.location.pathname.includes('/assist/');
|
||||
const isAssist = window.location.pathname.includes('/assist/');
|
||||
|
||||
const location = props.currentLocation && props.currentLocation.length > 60 ? `${props.currentLocation.slice(0, 60)}...` : props.currentLocation
|
||||
return (
|
||||
<div className="w-full px-4 py-2 flex items-center border-b">
|
||||
{location && (
|
||||
<div
|
||||
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
|
||||
onClick={() => {
|
||||
copy(props.currentLocation);
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 5000)
|
||||
}}
|
||||
>
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
hideOnClick={false}
|
||||
position="bottom center"
|
||||
title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}
|
||||
>
|
||||
{location}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{!isAssist ? (
|
||||
<div className="ml-auto text-sm flex items-center color-gray-medium" style={{ width: 'max-content' }}>
|
||||
<div className="cursor-pointer mr-4 hover:bg-gray-light-shade rounded-md p-1">
|
||||
{props.jiraConfig && props.jiraConfig.token && <Issues sessionId={props.sessionId} />}
|
||||
</div>
|
||||
<div className="cursor-pointer">
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={ props.sessionId }
|
||||
showCopyLink={true}
|
||||
trigger={
|
||||
<div className="flex items-center hover:bg-gray-light-shade rounded-md p-1">
|
||||
<Icon
|
||||
className="mr-2"
|
||||
disabled={ props.disabled }
|
||||
name="share-alt"
|
||||
size="16"
|
||||
/>
|
||||
<span>Share</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 hover:bg-gray-light-shade rounded-md p-1">
|
||||
<Bookmark noMargin sessionId={props.sessionId} />
|
||||
</div>
|
||||
<div>
|
||||
<Autoplay />
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
const location =
|
||||
props.currentLocation && props.currentLocation.length > 60
|
||||
? `${props.currentLocation.slice(0, 60)}...`
|
||||
: props.currentLocation;
|
||||
|
||||
const toggleNotePopup = () => {
|
||||
pause();
|
||||
};
|
||||
return (
|
||||
<div className="w-full px-4 py-2 flex items-center border-b">
|
||||
{location && (
|
||||
<div
|
||||
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
|
||||
onClick={() => {
|
||||
copy(props.currentLocation);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 5000);
|
||||
}}
|
||||
>
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
hideOnClick={false}
|
||||
position="bottom center"
|
||||
title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}
|
||||
>
|
||||
{location}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{!isAssist ? (
|
||||
<div
|
||||
className="ml-auto text-sm flex items-center color-gray-medium"
|
||||
style={{ width: 'max-content' }}
|
||||
>
|
||||
<div
|
||||
onClick={toggleNotePopup}
|
||||
className="cursor-pointer mr-4 hover:bg-gray-light-shade rounded-md p-1 flex items-center"
|
||||
>
|
||||
<Icon name="quotes" size="16" className="mr-2" />
|
||||
Add note
|
||||
</div>
|
||||
<div className="cursor-pointer mr-4 hover:bg-gray-light-shade rounded-md p-1">
|
||||
{props.jiraConfig && props.jiraConfig.token && <Issues sessionId={props.sessionId} />}
|
||||
</div>
|
||||
<div className="cursor-pointer">
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={props.sessionId}
|
||||
showCopyLink={true}
|
||||
trigger={
|
||||
<div className="flex items-center hover:bg-gray-light-shade rounded-md p-1">
|
||||
<Icon className="mr-2" disabled={props.disabled} name="share-alt" size="16" />
|
||||
<span>Share</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 hover:bg-gray-light-shade rounded-md p-1">
|
||||
<Bookmark noMargin sessionId={props.sessionId} />
|
||||
</div>
|
||||
<div>
|
||||
<Autoplay />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SubH = connectPlayer(state => ({ currentLocation: state.location }))(SubHeader)
|
||||
const SubH = connectPlayer((state) => ({ currentLocation: state.location }))(SubHeader);
|
||||
|
||||
export default React.memo(SubH)
|
||||
export default React.memo(SubH);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
classNam?: string;
|
||||
className?: string;
|
||||
label?: string;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -63,7 +63,8 @@ const initialState = Map({
|
|||
timelinePointer: null,
|
||||
sessionPath: {},
|
||||
lastPlayedSessionId: null,
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false }
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false },
|
||||
noteTooltip: { time: 100, offset: 100, isVisible: true },
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,23 @@ function error(...args) {
|
|||
}
|
||||
}
|
||||
|
||||
let groupTm = null;
|
||||
|
||||
function group(...args) {
|
||||
if (!window.env.PRODUCTION || options.verbose) {
|
||||
if (!groupTm) {
|
||||
groupTm = setTimeout(() => console.groupEnd(), 500)
|
||||
console.groupCollapsed('Openreplay: Skipping session messages')
|
||||
}
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
info: log,
|
||||
log,
|
||||
warn,
|
||||
error,
|
||||
group,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logger from 'App/logger';
|
|||
import RawMessageReader from './RawMessageReader';
|
||||
|
||||
// TODO: composition instead of inheritance
|
||||
// needSkipMessage() and next() methods here use buf and p protected properties,
|
||||
// needSkipMessage() and next() methods here use buf and p protected properties,
|
||||
// which should be probably somehow incapsulated
|
||||
export default class MFileReader extends RawMessageReader {
|
||||
private pLastMessageID: number = 0
|
||||
|
|
@ -49,7 +49,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
if (!skippedMessage) {
|
||||
return null
|
||||
}
|
||||
logger.log("Skipping message: ", skippedMessage)
|
||||
logger.group("Skipping message: ", skippedMessage)
|
||||
}
|
||||
|
||||
this.pLastMessageID = this.p
|
||||
|
|
@ -65,7 +65,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
}
|
||||
this.currentTime = rMsg.timestamp - this.startTime
|
||||
return this.next()
|
||||
}
|
||||
}
|
||||
|
||||
const msg = Object.assign(rMsg, {
|
||||
time: this.currentTime,
|
||||
|
|
|
|||
3
frontend/app/svg/icons/quotes.svg
Normal file
3
frontend/app/svg/icons/quotes.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 29 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.7" d="M11.6355 2.9045C10.8963 1.75328 9.80334 0.873101 8.521 0.396241C7.23867 -0.0806184 5.83621 -0.128393 4.5244 0.260101C3.21259 0.648595 2.06231 1.45237 1.24645 2.55061C0.430585 3.64885 -0.00678382 4.98223 7.95597e-05 6.35034C0.00076002 7.48328 0.305669 8.59524 0.882952 9.57006C1.46024 10.5449 2.28871 11.3468 3.28182 11.892C4.27493 12.4372 5.39624 12.7058 6.52859 12.6695C7.66095 12.6333 8.76279 12.2937 9.71903 11.6861C9.22188 13.1623 8.29591 14.7372 6.77033 16.316C6.47844 16.6179 6.31846 17.0234 6.32558 17.4433C6.3327 17.8632 6.50633 18.2631 6.80828 18.555C7.11022 18.8469 7.51575 19.0069 7.93566 18.9997C8.35556 18.9926 8.75543 18.819 9.04731 18.517C14.6867 12.6728 13.9542 6.31998 11.6355 2.91209V2.9045ZM26.8154 2.9045C26.0762 1.75328 24.9833 0.873101 23.7009 0.396241C22.4186 -0.0806184 21.0161 -0.128393 19.7043 0.260101C18.3925 0.648595 17.2422 1.45237 16.4264 2.55061C15.6105 3.64885 15.1731 4.98223 15.18 6.35034C15.1807 7.48328 15.4856 8.59524 16.0629 9.57006C16.6402 10.5449 17.4686 11.3468 18.4617 11.892C19.4549 12.4372 20.5762 12.7058 21.7085 12.6695C22.8409 12.6333 23.9427 12.2937 24.899 11.6861C24.4018 13.1623 23.4758 14.7372 21.9503 16.316C21.6584 16.6179 21.4984 17.0234 21.5055 17.4433C21.5126 17.8632 21.6863 18.2631 21.9882 18.555C22.2901 18.8469 22.6957 19.0069 23.1156 18.9997C23.5355 18.9926 23.9354 18.819 24.2272 18.517C29.8666 12.6728 29.1342 6.31998 26.8154 2.91209V2.9045Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Loading…
Add table
Reference in a new issue