feat(ui): fix adding/removing to the lists, add options popup, inject notes dynamically
This commit is contained in:
parent
266a0bef7e
commit
65a4b1ca93
23 changed files with 552 additions and 135 deletions
|
|
@ -26,6 +26,7 @@ const siteIdRequiredPaths = [
|
|||
'/dashboards',
|
||||
'/metrics',
|
||||
'/unprocessed',
|
||||
'/notes',
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ const noStoringFetchPathStarts = [
|
|||
|
||||
// null?
|
||||
export const clean = (obj, forbidenValues = [ undefined, '' ]) => {
|
||||
const keys = Array.isArray(obj)
|
||||
const keys = Array.isArray(obj)
|
||||
? new Array(obj.length).fill().map((_, i) => i)
|
||||
: Object.keys(obj);
|
||||
const retObj = Array.isArray(obj) ? [] : {};
|
||||
|
|
@ -49,7 +50,7 @@ export const clean = (obj, forbidenValues = [ undefined, '' ]) => {
|
|||
retObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return retObj;
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ export default class APIClient {
|
|||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
if (params !== undefined) {
|
||||
const cleanedParams = options.clean ? clean(params) : params;
|
||||
this.init.body = JSON.stringify(cleanedParams);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { connectPlayer } from 'Player';
|
||||
import cls from './timeTracker.module.css';
|
||||
|
||||
|
||||
function TimeTracker({ player, scale }) {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={ cls.positionTracker }
|
||||
style={ { left: `${ player.state.time * scale }%` } }
|
||||
/>
|
||||
<div
|
||||
className={ cls.playedTimeline }
|
||||
className={ cls.playedTimeline }
|
||||
style={ { width: `${ player.state.time * scale }%` } }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(TimeTracker);
|
||||
export default observer(TimeTracker);
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { connect } from 'react-redux';
|
|||
import { Loader } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import { PlayerProvider, connectPlayer, init as initPlayer, clean as cleanPlayer, Controls } from 'Player';
|
||||
import { PlayerProvider, injectNotes, connectPlayer, init as initPlayer, clean as cleanPlayer, Controls } from 'Player';
|
||||
import cn from 'classnames';
|
||||
import RightBlock from './RightBlock';
|
||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||
|
||||
import { useStore } from 'App/mstore'
|
||||
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
|
||||
import PlayerBlock from '../Session_/PlayerBlock';
|
||||
import styles from '../Session_/session.module.css';
|
||||
|
|
@ -62,6 +62,7 @@ function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) {
|
|||
|
||||
function WebPlayer(props) {
|
||||
const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props;
|
||||
const { notesStore } = useStore()
|
||||
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
|
||||
|
|
@ -69,6 +70,10 @@ function WebPlayer(props) {
|
|||
fetchList('issues');
|
||||
initPlayer(session, jwt);
|
||||
|
||||
notesStore.fetchSessionNotes(session.sessionId).then(r => {
|
||||
injectNotes(r)
|
||||
})
|
||||
|
||||
const jumptTime = props.query.get('jumpto');
|
||||
if (jumptTime) {
|
||||
Controls.jump(parseInt(jumptTime));
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import withToggle from 'HOCs/withToggle';
|
|||
import { TYPES } from 'Types/session/event';
|
||||
import Event from './Event'
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import NoteEvent from './NoteEvent';
|
||||
|
||||
// TODO: incapsulate toggler in LocationEvent
|
||||
@withToggle("showLoadInfo", "toggleLoadInfo")
|
||||
class EventGroupWrapper extends React.PureComponent {
|
||||
class EventGroupWrapper extends React.Component {
|
||||
|
||||
toggleLoadInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -42,6 +43,8 @@ class EventGroupWrapper extends React.PureComponent {
|
|||
showLoadInfo,
|
||||
isFirst,
|
||||
presentInSearch,
|
||||
isNote,
|
||||
filterOutNote,
|
||||
} = this.props;
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
||||
|
|
@ -64,7 +67,19 @@ class EventGroupWrapper extends React.PureComponent {
|
|||
</TextEllipsis>
|
||||
</div>
|
||||
}
|
||||
{ isLocation
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
userId={event.userId}
|
||||
timestamp={event.timestamp}
|
||||
tags={event.tags}
|
||||
isPublic={event.isPublic}
|
||||
message={event.message}
|
||||
sessionId={event.sessionId}
|
||||
date={event.createdAt}
|
||||
noteId={event.noteId}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
) : isLocation
|
||||
? <Event
|
||||
extended={isFirst}
|
||||
key={ event.key }
|
||||
|
|
@ -90,8 +105,7 @@ class EventGroupWrapper extends React.PureComponent {
|
|||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Icon } from 'UI';
|
|||
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized";
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setSelected } from 'Duck/events';
|
||||
import { setEventFilter } from 'Duck/sessions';
|
||||
import { setEventFilter, filterOutNote } from 'Duck/sessions';
|
||||
import { show as showTargetDefiner } from 'Duck/components/targetDefiner';
|
||||
import EventGroupWrapper from './EventGroupWrapper';
|
||||
import styles from './eventsBlock.module.css';
|
||||
|
|
@ -21,7 +21,8 @@ import EventSearch from './EventSearch/EventSearch';
|
|||
}), {
|
||||
showTargetDefiner,
|
||||
setSelected,
|
||||
setEventFilter
|
||||
setEventFilter,
|
||||
filterOutNote
|
||||
})
|
||||
export default class EventsBlock extends React.PureComponent {
|
||||
state = {
|
||||
|
|
@ -123,21 +124,28 @@ export default class EventsBlock extends React.PureComponent {
|
|||
onMouseOver = () => this.setState({ mouseOver: true })
|
||||
onMouseLeave = () => this.setState({ mouseOver: false })
|
||||
|
||||
get eventsList() {
|
||||
const { session: { notesWithEvents }, filteredEvents } = this.props
|
||||
const usedEvents = filteredEvents || notesWithEvents
|
||||
|
||||
return usedEvents
|
||||
}
|
||||
|
||||
renderGroup = ({ index, key, style, parent }) => {
|
||||
const {
|
||||
session: { events },
|
||||
selectedEvents,
|
||||
currentTimeEventIndex,
|
||||
testsAvaliable,
|
||||
playing,
|
||||
eventsIndex,
|
||||
filteredEvents
|
||||
filterOutNote,
|
||||
} = this.props;
|
||||
const { query } = this.state;
|
||||
const _events = filteredEvents || events;
|
||||
const _events = this.eventsList
|
||||
const isLastEvent = index === _events.size - 1;
|
||||
const isLastInGroup = isLastEvent || _events.get(index + 1).type === TYPES.LOCATION;
|
||||
const event = _events.get(index);
|
||||
const isNote = !!event.noteId
|
||||
const isSelected = selectedEvents.includes(event);
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
const isEditing = this.state.editingEvent === event;
|
||||
|
|
@ -166,6 +174,8 @@ export default class EventsBlock extends React.PureComponent {
|
|||
isCurrent={ isCurrent }
|
||||
isEditing={ isEditing }
|
||||
showSelection={ testsAvaliable && !playing }
|
||||
isNote={isNote}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
102
frontend/app/components/Session_/EventsBlock/NoteEvent.tsx
Normal file
102
frontend/app/components/Session_/EventsBlock/NoteEvent.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { tagProps, iTag } from 'App/services/NotesService';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ItemMenu } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { session } from 'App/routes';
|
||||
import { confirm } from 'UI';
|
||||
import { filterOutNote as filterOutTimelineNote } from 'Player';
|
||||
|
||||
interface Props {
|
||||
userId: number;
|
||||
timestamp: number;
|
||||
tags: iTag[];
|
||||
isPublic: boolean;
|
||||
message: string;
|
||||
sessionId: string;
|
||||
date: string;
|
||||
noteId: number;
|
||||
filterOutNote: (id: number) => void;
|
||||
}
|
||||
|
||||
function NoteEvent(props: Props) {
|
||||
const { settingsStore, notesStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
const onEdit = () => {};
|
||||
|
||||
const onCopy = () => {
|
||||
copy(`${session(props.sessionId)}${props.timestamp > 0 ? '?jumpto=' + props.timestamp : ''}`);
|
||||
toast.success('Note URL copied to clipboard');
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to delete this note?`,
|
||||
})
|
||||
) {
|
||||
notesStore.deleteNote(props.noteId).then((r) => {
|
||||
props.filterOutNote(props.noteId);
|
||||
filterOutTimelineNote(props.noteId)
|
||||
toast.success('Note deleted');
|
||||
});
|
||||
}
|
||||
};
|
||||
const menuItems = [
|
||||
{ icon: 'pencil', text: 'Edit', onClick: onEdit },
|
||||
{ icon: 'link-45deg', text: 'Copy URL', onClick: onCopy },
|
||||
{ icon: 'trash', text: 'Delete', onClick: onDelete },
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="flex items-start flex-col p-2 border"
|
||||
style={{ background: 'rgba(253, 243, 155, 0.1)' }}
|
||||
>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="p-2 bg-gray-light rounded-full">
|
||||
<Icon name="quotes" color="main" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div>{props.userId}</div>
|
||||
<div className="text-disabled-text">
|
||||
{formatTimeOrDate(props.date as unknown as number, timezone)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cursor-pointer ml-auto">
|
||||
<ItemMenu bolt items={menuItems} />
|
||||
</div>
|
||||
</div>
|
||||
<div>{props.message}</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{props.tags.length ? (
|
||||
<div className="flex items-center gap-1">
|
||||
{props.tags.map((tag) => (
|
||||
<div
|
||||
style={{ background: tagProps[tag], userSelect: 'none' }}
|
||||
className="rounded-xl text-sm px-2 py-1 text-white"
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{!props.isPublic ? null : (
|
||||
<>
|
||||
<Icon name="user-friends" className="ml-2 mr-1" color="gray-dark" /> Team
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(NoteEvent);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Icon } from 'UI'
|
||||
import { connectPlayer, Controls, toggleTimetravel } from 'Player';
|
||||
import TimeTracker from './TimeTracker';
|
||||
import stl from './timeline.module.css';
|
||||
|
|
@ -30,22 +31,12 @@ let debounceTooltipChange = () => null;
|
|||
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
|
||||
endTime: state.endTime,
|
||||
live: state.live,
|
||||
logList: state.logList,
|
||||
exceptionsList: state.exceptionsList,
|
||||
resourceList: state.resourceList,
|
||||
stackList: state.stackList,
|
||||
fetchList: state.fetchList,
|
||||
notes: state.notes,
|
||||
}))
|
||||
@connect(
|
||||
(state) => ({
|
||||
issues: state.getIn(['sessions', 'current', 'issues']),
|
||||
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
|
||||
clickRageTime:
|
||||
state.getIn(['sessions', 'current', 'clickRage']) &&
|
||||
state.getIn(['sessions', 'current', 'clickRageTime']),
|
||||
returningLocationTime:
|
||||
state.getIn(['sessions', 'current', 'returningLocation']) &&
|
||||
state.getIn(['sessions', 'current', 'returningLocationTime']),
|
||||
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
|
||||
}),
|
||||
{ setTimelinePointer, setTimelineHoverTime }
|
||||
|
|
@ -170,7 +161,7 @@ export default class Timeline extends React.PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { events, skip, skipIntervals, disabled, endTime, live } = this.props;
|
||||
const { events, skip, skipIntervals, disabled, endTime, live, notes } = this.props;
|
||||
|
||||
const scale = 100 / endTime;
|
||||
|
||||
|
|
@ -228,6 +219,20 @@ export default class Timeline extends React.PureComponent {
|
|||
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
||||
/>
|
||||
))}
|
||||
{notes.map((note) => note.timestamp > 0 ? (
|
||||
<div
|
||||
key={note.noteId}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: 'white',
|
||||
zIndex: 3,
|
||||
pointerEvents: 'none',
|
||||
left: `${getTimelinePosition(note.timestamp, scale)}%`,
|
||||
}}
|
||||
>
|
||||
<Icon name="quotes" size={16} color="main" />
|
||||
</div>
|
||||
) : null)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,72 +2,138 @@ import React from 'react';
|
|||
import { Icon, Button, Checkbox } from 'UI';
|
||||
import { Duration } from 'luxon';
|
||||
import { connect } from 'react-redux';
|
||||
import { WriteNote, tagProps, TAGS, iTag, Note } from 'App/services/NotesService'
|
||||
import { setNoteTooltip, addNote } from 'Duck/sessions';
|
||||
import stl from './styles.module.css';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { toast } from 'react-toastify';
|
||||
import { injectNotes } from 'Player';
|
||||
|
||||
interface Props {
|
||||
offset: number;
|
||||
isVisible: boolean;
|
||||
time: number;
|
||||
setNoteTooltip: (state: any) => void
|
||||
addNote: (note: Note) => void
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
const TAGS = ['QUERY', 'ISSUE', 'TASK', 'OTHER'];
|
||||
function NoteTooltip({ isVisible, time, setNoteTooltip, sessionId, addNote }: Props) {
|
||||
const [text, setText] = React.useState('')
|
||||
const [isPublic, setPublic] = React.useState(false)
|
||||
const [tags, setTags] = React.useState([])
|
||||
const [useTimestamp, setUseTs] = React.useState(false)
|
||||
|
||||
const { notesStore } = useStore();
|
||||
|
||||
function NoteTooltip({ offset, isVisible, time }: Props) {
|
||||
const duration = Duration.fromMillis(time).toFormat('mm:ss');
|
||||
|
||||
const stopEvents = (e: any) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
const note: WriteNote = {
|
||||
message: text,
|
||||
tags,
|
||||
timestamp: useTimestamp ? time : -1,
|
||||
isPublic,
|
||||
}
|
||||
|
||||
notesStore.addNote(sessionId, note).then(r => {
|
||||
toast.success('Note added')
|
||||
notesStore.fetchSessionNotes(sessionId).then(notes => {
|
||||
injectNotes(notes)
|
||||
addNote(r)
|
||||
})
|
||||
}).catch(e => {
|
||||
toast.error('Error adding note')
|
||||
console.error(e)
|
||||
}).finally(() => {
|
||||
setNoteTooltip({ isVisible: false, time: 0 })
|
||||
setText('')
|
||||
setTags([])
|
||||
})
|
||||
}
|
||||
|
||||
const closeTooltip = () => {
|
||||
setNoteTooltip({ isVisible: false, time: 0 })
|
||||
}
|
||||
|
||||
const tagActive = (tag: iTag) => tags.includes(tag)
|
||||
const removeTag = (tag: iTag) => {
|
||||
setTags(tags.filter(t => t !== tag))
|
||||
}
|
||||
const addTag = (tag: iTag) => {
|
||||
setTags([...tags, tag])
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={stl.noteTooltip}
|
||||
style={{
|
||||
top: -250,
|
||||
top: -260,
|
||||
width: 350,
|
||||
left: offset - 20,
|
||||
display: isVisible ? 'block' : 'none',
|
||||
left: 'calc(50% - 175px)',
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}}
|
||||
onClick={stopEvents}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center bg-gray-lightest">
|
||||
<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 />
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setUseTs(!useTimestamp)}>
|
||||
<Checkbox checked={useTimestamp} />
|
||||
<span className="ml-1">{`at ${duration}`}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto cursor-pointer">
|
||||
<div className="ml-auto cursor-pointer" onClick={closeTooltip}>
|
||||
<Icon name="close" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>text field</div>
|
||||
<div className="">
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
placeholder="Note..."
|
||||
rows={3}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
style={{ border: 'solid thin #ddd', borderRadius: 3, resize: 'none', background: '#ffff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-4">
|
||||
<div className="flex items-center">
|
||||
{TAGS.map((tag) => (
|
||||
<div className="rounded-xl px-2 py-1 mr-2 bg-gray-medium"> {tag} </div>
|
||||
<div
|
||||
key={tag}
|
||||
style={{ background: tagActive(tag) ? tagProps[tag] : 'rgba(0,0,0, 0.38)', userSelect: 'none' }}
|
||||
className="cursor-pointer rounded-xl px-2 py-1 mr-2 text-white"
|
||||
onClick={() => tagActive(tag) ? removeTag(tag) : addTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-4">
|
||||
<Button variant="primary" className="mr-4">
|
||||
<div className="flex">
|
||||
<Button variant="primary" className="mr-4" onClick={onSubmit}>
|
||||
Add Note
|
||||
</Button>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Checkbox />
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setPublic(!isPublic)}>
|
||||
<Checkbox checked={isPublic} />
|
||||
<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);
|
||||
// @ts-ignore
|
||||
const { isVisible, time = 0 } = state.getIn(['sessions', 'noteTooltip']);
|
||||
// @ts-ignore
|
||||
const sessionId = state.getIn(['sessions', 'current', 'sessionId'])
|
||||
return { isVisible, time, sessionId };
|
||||
}, { setNoteTooltip, addNote })(NoteTooltip);
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ function PlayerControls(props: Props) {
|
|||
</div>
|
||||
{Object.keys(skipIntervals).map((interval) => (
|
||||
<div
|
||||
key={interval}
|
||||
onClick={() => {
|
||||
toggleTooltip();
|
||||
setSkipInterval(parseInt(interval, 10));
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { Icon } from 'UI';
|
|||
import Autoplay from './Autoplay';
|
||||
import Bookmark from 'Shared/Bookmark';
|
||||
import SharePopup from '../shared/SharePopup/SharePopup';
|
||||
import { connectPlayer, pause } from 'Player';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import Issues from './Issues/Issues';
|
||||
import NotePopup from './components/NotePopup';
|
||||
import { connectPlayer } from 'Player';
|
||||
|
||||
function SubHeader(props) {
|
||||
const [isCopied, setCopied] = React.useState(false);
|
||||
|
|
@ -18,9 +19,6 @@ function SubHeader(props) {
|
|||
? `${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 && (
|
||||
|
|
@ -50,13 +48,7 @@ function SubHeader(props) {
|
|||
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>
|
||||
<NotePopup />
|
||||
<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>
|
||||
|
|
@ -86,6 +78,8 @@ function SubHeader(props) {
|
|||
);
|
||||
}
|
||||
|
||||
const SubH = connectPlayer((state) => ({ currentLocation: state.location }))(SubHeader);
|
||||
const SubH = connectPlayer(
|
||||
(state) => ({ currentLocation: state.location })
|
||||
)(SubHeader);
|
||||
|
||||
export default React.memo(SubH);
|
||||
|
|
|
|||
34
frontend/app/components/Session_/components/NotePopup.tsx
Normal file
34
frontend/app/components/Session_/components/NotePopup.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import { connectPlayer, pause } from 'Player';
|
||||
import { connect } from 'react-redux'
|
||||
import { setNoteTooltip } from 'Duck/sessions';
|
||||
|
||||
function NotePopup({ setNoteTooltip, time }: { setNoteTooltip: (args: any) => void, time: number }) {
|
||||
const toggleNotePopup = () => {
|
||||
pause();
|
||||
setNoteTooltip({ time: time, isVisible: true })
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const NotePopupPl = connectPlayer(
|
||||
// @ts-ignore
|
||||
(state) => ({ time: state.time })
|
||||
)(React.memo(NotePopup));
|
||||
|
||||
const NotePopupComp = connect(
|
||||
null, { setNoteTooltip }
|
||||
)(NotePopupPl)
|
||||
|
||||
export default React.memo(NotePopupComp)
|
||||
|
|
@ -15,7 +15,7 @@ interface Props {
|
|||
viewed: boolean;
|
||||
sessionId: string;
|
||||
onClick?: () => void;
|
||||
queryParams: any;
|
||||
queryParams?: any;
|
||||
}
|
||||
export default function PlayLink(props: Props) {
|
||||
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
||||
|
|
|
|||
|
|
@ -2,16 +2,36 @@ import React from 'react';
|
|||
import SessionList from './components/SessionList';
|
||||
import SessionHeader from './components/SessionHeader';
|
||||
import NotesList from './components/Notes/NoteList';
|
||||
import { connect } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchList as fetchMembers } from 'Duck/member';
|
||||
|
||||
function SessionListContainer({ activeTab }: { activeTab: string }) {
|
||||
return (
|
||||
<div className="widget-wrapper">
|
||||
<SessionHeader />
|
||||
<div className="border-b" />
|
||||
{activeTab !== 'notes' ? <SessionList /> : <NotesList />}
|
||||
</div>
|
||||
);
|
||||
function SessionListContainer({
|
||||
activeTab,
|
||||
fetchMembers,
|
||||
members,
|
||||
}: {
|
||||
activeTab: string;
|
||||
fetchMembers: () => void;
|
||||
members: object[];
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
fetchMembers();
|
||||
}, []);
|
||||
return (
|
||||
<div className="widget-wrapper">
|
||||
<SessionHeader />
|
||||
<div className="border-b" />
|
||||
{activeTab !== 'notes' ? <SessionList /> : <NotesList members={members} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({ activeTab: state.getIn(['search', 'activeTab', 'type'])}))(SessionListContainer);
|
||||
export default connect(
|
||||
(state) => ({
|
||||
// @ts-ignore
|
||||
activeTab: state.getIn(['search', 'activeTab', 'type']),
|
||||
// @ts-ignore
|
||||
members: state.getIn(['members', 'list']),
|
||||
}),
|
||||
{ fetchMembers }
|
||||
)(SessionListContainer);
|
||||
|
|
|
|||
|
|
@ -1,46 +1,92 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import PlayLink from 'Shared/SessionItem/PlayLink'
|
||||
|
||||
enum Tags {
|
||||
QUERY,
|
||||
ISSUE,
|
||||
TASK,
|
||||
OTHER
|
||||
}
|
||||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import PlayLink from 'Shared/SessionItem/PlayLink';
|
||||
import { tagProps, iTag } from 'App/services/NotesService';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ItemMenu } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { session } from 'App/routes'
|
||||
|
||||
interface Props {
|
||||
author: string
|
||||
timestamp: number
|
||||
tags: string[]
|
||||
isPrivate: boolean
|
||||
description: string
|
||||
sessionId: string
|
||||
userId: number;
|
||||
timestamp: number;
|
||||
tags: iTag[];
|
||||
isPublic: boolean;
|
||||
description: string;
|
||||
sessionId: string;
|
||||
date: string;
|
||||
noteId: number;
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
function NoteItem(props: Props) {
|
||||
const { settingsStore, notesStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
const onCopy = () => {
|
||||
copy(`${window.location.origin}${session(props.sessionId)}${props.timestamp > 0 ? '?jumpto=' + props.timestamp : ''}`);
|
||||
toast.success('Note URL copied to clipboard')
|
||||
}
|
||||
const onDelete = () => {
|
||||
notesStore.deleteNote(props.noteId).then(r => {
|
||||
notesStore.fetchNotes()
|
||||
toast.success('Note deleted')
|
||||
})
|
||||
};
|
||||
const menuItems = [
|
||||
{ icon: 'link-45deg', text: 'Copy URL', onClick: onCopy },
|
||||
{ icon: 'trash', text: 'Delete', onClick: onDelete },
|
||||
]
|
||||
return (
|
||||
<div className="flex items-center p-4 border-b hover:backdrop-opacity-25" style={{ background: 'rgba(253, 243, 155, 0.1)' }}>
|
||||
<div className="flex flex-col">
|
||||
<div>{props.description}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div>{props.tags}</div>
|
||||
<div className='text-disabled-text flex items-center'>
|
||||
<div
|
||||
className="flex items-center p-4 border-b"
|
||||
style={{ background: 'rgba(253, 243, 155, 0.1)' }}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>{props.description}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{props.tags.length ? (
|
||||
<div className="flex items-center gap-1">
|
||||
{props.tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
style={{ background: tagProps[tag], userSelect: 'none' }}
|
||||
className="rounded-xl px-2 py-1 mr-2 text-white"
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-disabled-text flex items-center">
|
||||
<span className="text-figmaColors-text-primary mr-1">By </span>
|
||||
{props.author}, {props.timestamp}
|
||||
{props.isPrivate ? null : (
|
||||
{props.userEmail}, {formatTimeOrDate(props.date as unknown as number, timezone)}
|
||||
{!props.isPublic ? null : (
|
||||
<>
|
||||
<Icon name="user-friends" className="ml-4 mr-1" color="gray-dark" /> Team
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto"><PlayLink isAssist={false} viewed={false} sessionId={props.sessionId} queryParams="noteurlparams" /></div>
|
||||
<div className="ml-2 cursor-pointer"><Icon name="ellipsis-v" size={20} /></div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<PlayLink
|
||||
isAssist={false}
|
||||
viewed={false}
|
||||
sessionId={props.sessionId + (props.timestamp > 0 ? '?jumpto=' + props.timestamp : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 cursor-pointer">
|
||||
<ItemMenu
|
||||
bold
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoteItem
|
||||
export default observer(NoteItem);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import NoteItem from './NoteItem';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
function NotesList() {
|
||||
function NotesList({ members }: {members: Array<Record<string, any>>}) {
|
||||
const { notesStore } = useStore()
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -15,7 +15,6 @@ function NotesList() {
|
|||
}, [])
|
||||
|
||||
const list = notesStore.notes
|
||||
console.log(list)
|
||||
return (
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
|
|
@ -30,18 +29,21 @@ function NotesList() {
|
|||
{sliceListPerPage(list, notesStore.page - 1, notesStore.pageSize).map(note => (
|
||||
<React.Fragment key={note.noteId}>
|
||||
<NoteItem
|
||||
author={note.author}
|
||||
userId={note.userId}
|
||||
tags={note.tags}
|
||||
timestamp={note.timestamp}
|
||||
isPrivate={note.isPublic}
|
||||
isPublic={note.isPublic}
|
||||
description={note.message}
|
||||
sessionId={'123123'} // note.sessionId
|
||||
date={note.createdAt}
|
||||
noteId={note.noteId}
|
||||
sessionId={note.sessionId}
|
||||
userEmail={members.find(m => m.id === note.userId).email || note.userId}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4 px-6">
|
||||
<div className="w-full flex items-center justify-between py-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing <span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
|
||||
of <span className="font-semibold">{list.length}</span> notes
|
||||
|
|
|
|||
|
|
@ -56,13 +56,12 @@ export default class ItemMenu extends React.PureComponent {
|
|||
<div className={cn(styles.menu, { [styles.menuDim]: !bold })} data-displayed={displayed}>
|
||||
{items
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(({ onClick, text, icon, disabled = false, disabledMessage = '' }) => (
|
||||
.map(({ onClick, text, icon, disabled = false }) => (
|
||||
<div
|
||||
key={text}
|
||||
onClick={!disabled ? this.onClick(onClick) : () => {}}
|
||||
className={disabled ? 'cursor-not-allowed' : ''}
|
||||
role="menuitem"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div className={cn(styles.menuItem, 'text-neutral-700', { disabled: disabled })}>
|
||||
{icon && (
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ export const reduceThenFetchResource =
|
|||
const filter = getState().getIn(['search', 'instance']).toData();
|
||||
|
||||
const activeTab = getState().getIn(['search', 'activeTab']);
|
||||
|
||||
if (activeTab.type === 'notes') return;
|
||||
if (activeTab.type !== 'all' && activeTab.type !== 'bookmark' && activeTab.type !== 'vault') {
|
||||
const tmpFilter = filtersMap[FilterKey.ISSUE];
|
||||
tmpFilter.value = [activeTab.type];
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
|
|||
const SET_FUNNEL_PAGE_FLAG = 'sessions/SET_FUNNEL_PAGE_FLAG';
|
||||
const SET_TIMELINE_POINTER = 'sessions/SET_TIMELINE_POINTER';
|
||||
const SET_TIMELINE_HOVER_POINTER = 'sessions/SET_TIMELINE_HOVER_POINTER';
|
||||
const SET_NOTE_TOOLTIP = 'sessions/SET_NOTE_TOOLTIP'
|
||||
const FILTER_OUT_NOTE = 'sessions/FILTER_OUT_NOTE'
|
||||
const ADD_NOTE = 'sessions/ADD_NOTE'
|
||||
|
||||
const SET_SESSION_PATH = 'sessions/SET_SESSION_PATH';
|
||||
const LAST_PLAYED_SESSION_ID = `${name}/LAST_PLAYED_SESSION_ID`;
|
||||
|
|
@ -64,7 +67,7 @@ const initialState = Map({
|
|||
sessionPath: {},
|
||||
lastPlayedSessionId: null,
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false },
|
||||
noteTooltip: { time: 100, offset: 100, isVisible: true },
|
||||
noteTooltip: { time: 0, isVisible: false },
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
|
|
@ -193,6 +196,22 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return state.set('timelinePointer', action.pointer);
|
||||
case SET_TIMELINE_HOVER_POINTER:
|
||||
return state.set('timeLineTooltip', action.timeLineTooltip);
|
||||
case SET_NOTE_TOOLTIP:
|
||||
return state.set('noteTooltip', action.noteTooltip);
|
||||
case FILTER_OUT_NOTE:
|
||||
return state.updateIn(['current', 'notesWithEvents'], (list) =>
|
||||
list.filter(evt => !evt.noteId || evt.noteId !== action.noteId)
|
||||
)
|
||||
case ADD_NOTE:
|
||||
console.log(action.note)
|
||||
return state.updateIn(['current', 'notesWithEvents'], (list) =>
|
||||
list.push(action.note).sort((a, b) => {
|
||||
const aTs = a.time || a.timestamp
|
||||
const bTs = b.time || b.timestamp
|
||||
|
||||
return aTs - bTs
|
||||
})
|
||||
)
|
||||
case SET_SESSION_PATH:
|
||||
return state.set('sessionPath', action.path);
|
||||
case LAST_PLAYED_SESSION_ID:
|
||||
|
|
@ -363,6 +382,27 @@ export function setTimelineHoverTime(timeLineTooltip) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setNoteTooltip(noteTooltip) {
|
||||
return {
|
||||
type: SET_NOTE_TOOLTIP,
|
||||
noteTooltip
|
||||
}
|
||||
}
|
||||
|
||||
export function filterOutNote(noteId) {
|
||||
return {
|
||||
type: FILTER_OUT_NOTE,
|
||||
noteId
|
||||
}
|
||||
}
|
||||
|
||||
export function addNote(note) {
|
||||
return {
|
||||
type: ADD_NOTE,
|
||||
note
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionPath(path) {
|
||||
return {
|
||||
type: SET_SESSION_PATH,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { makeAutoObservable } from "mobx"
|
||||
import { notesService } from "App/services"
|
||||
import { Note } from 'App/services/NotesService'
|
||||
import { Note, WriteNote } from 'App/services/NotesService'
|
||||
|
||||
interface SessionNotes {
|
||||
[sessionId: string]: Note[]
|
||||
|
|
@ -8,7 +8,7 @@ interface SessionNotes {
|
|||
|
||||
export default class NotesStore {
|
||||
notes: Note[] = []
|
||||
sessionNotes: SessionNotes
|
||||
sessionNotes: SessionNotes = {}
|
||||
loading: boolean
|
||||
page = 1
|
||||
pageSize = 15
|
||||
|
|
@ -35,6 +35,7 @@ export default class NotesStore {
|
|||
try {
|
||||
const notes = await notesService.getNotesBySessionId(sessionId)
|
||||
this.sessionNotes[sessionId] = notes
|
||||
return notes;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
|
|
@ -42,7 +43,7 @@ export default class NotesStore {
|
|||
}
|
||||
}
|
||||
|
||||
async addNote(sessionId: string, note: Note) {
|
||||
async addNote(sessionId: string, note: WriteNote) {
|
||||
this.loading = true
|
||||
try {
|
||||
const addedNote = await notesService.addNote(sessionId, note)
|
||||
|
|
@ -54,7 +55,7 @@ export default class NotesStore {
|
|||
}
|
||||
}
|
||||
|
||||
async deleteNote(noteId: string) {
|
||||
async deleteNote(noteId: number) {
|
||||
this.loading = true
|
||||
try {
|
||||
const deleted = await notesService.deleteNote(noteId)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { goTo as listsGoTo } from './lists';
|
||||
import { update, getState } from './store';
|
||||
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE } from './MessageDistributor/MessageDistributor';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
|
||||
const fps = 60;
|
||||
const performance = window.performance || { now: Date.now.bind(Date) };
|
||||
|
|
@ -46,6 +47,7 @@ export const INITIAL_STATE = {
|
|||
live: false,
|
||||
livePlay: false,
|
||||
liveTimeTravel: false,
|
||||
notes: [],
|
||||
} as const;
|
||||
|
||||
|
||||
|
|
@ -269,6 +271,15 @@ export default class Player extends MessageDistributor {
|
|||
this.cursor.toggleUserName(name)
|
||||
}
|
||||
|
||||
injectNotes(notes: Note[]) {
|
||||
update({ notes })
|
||||
}
|
||||
|
||||
filterOutNote(noteId: number) {
|
||||
const { notes } = getState()
|
||||
update({ notes: notes.filter((note: Note) => note.noteId !== noteId) })
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.pause();
|
||||
super.clean();
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ export const toggleAnnotation = initCheck((...args) => instance.assistManager.to
|
|||
export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args))
|
||||
export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args))
|
||||
export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args))
|
||||
export const injectNotes = initCheck((...args) => instance.injectNotes(...args))
|
||||
export const filterOutNote = initCheck((...args) => instance.filterOutNote(...args))
|
||||
|
||||
export const Controls = {
|
||||
jump,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
|
||||
|
||||
import APIClient from 'App/api_client';
|
||||
|
||||
export interface Note {
|
||||
|
||||
export const tagProps = {
|
||||
'QUERY': '#3EAAAF',
|
||||
'ISSUE': '#CC0000',
|
||||
'TASK': '#7986CB',
|
||||
'OTHER': 'rgba(0, 0, 0, 0.26)',
|
||||
}
|
||||
|
||||
export type iTag = keyof typeof tagProps
|
||||
|
||||
export const TAGS = Object.keys(tagProps) as unknown as (keyof typeof tagProps)[]
|
||||
|
||||
export interface WriteNote {
|
||||
message: string
|
||||
tags: string[]
|
||||
isPublic: boolean
|
||||
|
|
@ -11,6 +21,19 @@ export interface Note {
|
|||
author?: string
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
createdAt: string
|
||||
deletedAt: string | null
|
||||
isPublic: boolean
|
||||
message: string
|
||||
noteId: number
|
||||
projectId: number
|
||||
sessionId: string
|
||||
tags: iTag[]
|
||||
timestamp: number
|
||||
userId: number
|
||||
}
|
||||
|
||||
export default class NotesService {
|
||||
private client: APIClient;
|
||||
|
||||
|
|
@ -25,7 +48,7 @@ export default class NotesService {
|
|||
getNotes(): Promise<Note[]> {
|
||||
return this.client.get('/notes').then(r => {
|
||||
if (r.ok) {
|
||||
return r.json()
|
||||
return r.json().then(r => r.data)
|
||||
} else {
|
||||
throw new Error('Error getting notes: ' + r.status)
|
||||
}
|
||||
|
|
@ -34,21 +57,45 @@ export default class NotesService {
|
|||
|
||||
getNotesBySessionId(sessionID: string): Promise<Note[]> {
|
||||
return this.client.get(`/sessions/${sessionID}/notes`)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(r => r.data)
|
||||
} else {
|
||||
throw new Error('Error getting notes for ' +sessionID + ' cuz: ' + r.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addNote(sessionID: string, note: Note): Promise<Note> {
|
||||
addNote(sessionID: string, note: WriteNote): Promise<Note> {
|
||||
return this.client.post(`/sessions/${sessionID}/notes`, note)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(r => r.data)
|
||||
} else {
|
||||
throw new Error('Error adding note: ' + r.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateNote(noteID: string, note: Note): Promise<Note> {
|
||||
return this.client.post(`/notes/${noteID}`, note)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(r => r.data)
|
||||
} else {
|
||||
throw new Error('Error updating note: ' + r.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteNote(noteID: string) {
|
||||
deleteNote(noteID: number) {
|
||||
return this.client.delete(`/notes/${noteID}`)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(r => r.data)
|
||||
} else {
|
||||
throw new Error('Error deleting note: ' + r.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import Record from 'Types/Record';
|
||||
import { List, Map } from 'immutable';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Duration } from 'luxon';
|
||||
import SessionEvent, { TYPES } from './event';
|
||||
import Log from './log';
|
||||
import StackEvent from './stackEvent';
|
||||
import Resource from './resource';
|
||||
import CustomField from './customField';
|
||||
import SessionError from './error';
|
||||
import Issue from './issue';
|
||||
|
||||
const SOURCE_JS = 'js_exception';
|
||||
|
||||
const HASH_MOD = 1610612741;
|
||||
const HASH_P = 53;
|
||||
function hashString(s: string): number {
|
||||
|
|
@ -82,7 +79,9 @@ export default Record({
|
|||
userSessionsCount: 0,
|
||||
agentIds: [],
|
||||
isCallActive: false,
|
||||
agentToken: ''
|
||||
agentToken: '',
|
||||
notes: [],
|
||||
notesWithEvents: [],
|
||||
}, {
|
||||
fromJS:({
|
||||
startTs=0,
|
||||
|
|
@ -93,9 +92,11 @@ export default Record({
|
|||
errors,
|
||||
stackEvents = [],
|
||||
issues = [],
|
||||
sessionId, sessionID,
|
||||
sessionId,
|
||||
sessionID,
|
||||
domURL = [],
|
||||
mobsUrl = [],
|
||||
notes = [],
|
||||
...session
|
||||
}) => {
|
||||
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
|
||||
|
|
@ -130,6 +131,19 @@ export default Record({
|
|||
const issuesList = List(issues)
|
||||
.map(e => Issue({ ...e, time: e.timestamp - startedAt }))
|
||||
|
||||
|
||||
const rawEvents = !session.events
|
||||
? []
|
||||
// @ts-ignore
|
||||
: session.events.map(evt => ({ ...evt, time: evt.timestamp - startedAt })).filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds) || []
|
||||
const rawNotes = notes
|
||||
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
|
||||
const aTs = a.time || a.timestamp
|
||||
const bTs = b.time || b.timestamp
|
||||
|
||||
return aTs - bTs
|
||||
})
|
||||
|
||||
return {
|
||||
...session,
|
||||
isIOS: session.platform === "ios",
|
||||
|
|
@ -154,6 +168,8 @@ export default Record({
|
|||
userId: session.userId || session.userID,
|
||||
domURL: Array.isArray(domURL) ? domURL : [ domURL ],
|
||||
mobsUrl: Array.isArray(mobsUrl) ? mobsUrl : [ mobsUrl ],
|
||||
notes,
|
||||
notesWithEvents: List(notesWithEvents),
|
||||
};
|
||||
},
|
||||
idKey: "sessionId",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue