change(ui): fix editing, sorting by backend etc
This commit is contained in:
parent
84ec8f4d76
commit
f3872b83a0
14 changed files with 452 additions and 225 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader } from 'UI';
|
||||
import { Loader, Modal } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import { PlayerProvider, injectNotes, connectPlayer, init as initPlayer, clean as cleanPlayer, Controls } from 'Player';
|
||||
|
|
@ -12,6 +12,8 @@ import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
|
|||
import PlayerBlock from '../Session_/PlayerBlock';
|
||||
import styles from '../Session_/session.module.css';
|
||||
import { countDaysFrom } from 'App/date';
|
||||
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
||||
import { fetchList as fetchMembers } from 'Duck/member';
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'User Actions',
|
||||
|
|
@ -63,15 +65,23 @@ 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('');
|
||||
const [showNoteModal, setShowNote] = useState(false)
|
||||
const [noteItem, setNoteItem] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchList('issues');
|
||||
initPlayer(session, jwt);
|
||||
props.fetchMembers()
|
||||
|
||||
notesStore.fetchSessionNotes(session.sessionId).then(r => {
|
||||
injectNotes(r)
|
||||
const note = props.query.get('note');
|
||||
if (note) {
|
||||
Controls.pause()
|
||||
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r))
|
||||
setShowNote(true)
|
||||
}
|
||||
})
|
||||
|
||||
const jumptTime = props.query.get('jumpto');
|
||||
|
|
@ -91,11 +101,27 @@ function WebPlayer(props) {
|
|||
[]
|
||||
);
|
||||
|
||||
const onNoteClose = () => {setShowNote(false); Controls.togglePlay()}
|
||||
return (
|
||||
<PlayerProvider>
|
||||
<InitLoader className="flex-1">
|
||||
<PlayerBlockHeader activeTab={activeTab} setActiveTab={setActiveTab} tabs={TABS} fullscreen={fullscreen} />
|
||||
<PlayerContentConnected activeTab={activeTab} fullscreen={fullscreen} live={live} setActiveTab={setActiveTab} session={session} />
|
||||
<Modal open={showNoteModal} onClose={onNoteClose}>
|
||||
{showNoteModal ? (
|
||||
<ReadNote
|
||||
userEmail={props.members.find(m => m.id === noteItem.userId)?.email || noteItem.userId}
|
||||
timestamp={noteItem.timestamp}
|
||||
tags={noteItem.tags}
|
||||
isPublic={noteItem.isPublic}
|
||||
message={noteItem.message}
|
||||
sessionId={noteItem.sessionId}
|
||||
date={noteItem.createdAt}
|
||||
noteId={noteItem.noteId}
|
||||
onClose={onNoteClose}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
</InitLoader>
|
||||
</PlayerProvider>
|
||||
);
|
||||
|
|
@ -107,10 +133,12 @@ export default connect(
|
|||
jwt: state.get('jwt'),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
showEvents: state.get('showEvents'),
|
||||
members: state.getIn(['members', 'list']),
|
||||
}),
|
||||
{
|
||||
toggleFullscreen,
|
||||
closeBottomBlock,
|
||||
fetchList,
|
||||
fetchMembers,
|
||||
}
|
||||
)(withLocationHandlers()(WebPlayer));
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
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 stl from './eventGroupWrapper.module.css';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import { setEditNoteTooltip } from 'Duck/sessions';
|
||||
|
||||
// TODO: incapsulate toggler in LocationEvent
|
||||
@withToggle("showLoadInfo", "toggleLoadInfo")
|
||||
@connect(state => ({members: state.getIn(['members', 'list'])}), { setEditNoteTooltip })
|
||||
class EventGroupWrapper extends React.Component {
|
||||
|
||||
toggleLoadInfo = (e) => {
|
||||
|
|
@ -69,7 +71,7 @@ class EventGroupWrapper extends React.Component {
|
|||
}
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
userId={event.userId}
|
||||
userEmail={this.props.members.find(m => m.id === event.userId)?.email || event.userId}
|
||||
timestamp={event.timestamp}
|
||||
tags={event.tags}
|
||||
isPublic={event.isPublic}
|
||||
|
|
@ -78,6 +80,7 @@ class EventGroupWrapper extends React.Component {
|
|||
date={event.createdAt}
|
||||
noteId={event.noteId}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
/>
|
||||
) : isLocation
|
||||
? <Event
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { confirm } from 'UI';
|
|||
import { filterOutNote as filterOutTimelineNote } from 'Player';
|
||||
|
||||
interface Props {
|
||||
userId: number;
|
||||
userEmail: number;
|
||||
timestamp: number;
|
||||
tags: iTag[];
|
||||
isPublic: boolean;
|
||||
|
|
@ -21,16 +21,35 @@ interface Props {
|
|||
date: string;
|
||||
noteId: number;
|
||||
filterOutNote: (id: number) => void;
|
||||
onEdit: (noteTooltipObj: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
function NoteEvent(props: Props) {
|
||||
const { settingsStore, notesStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
const onEdit = () => {};
|
||||
const onEdit = () => {
|
||||
props.onEdit({
|
||||
isVisible: true,
|
||||
isEdit: true,
|
||||
time: props.timestamp,
|
||||
note: {
|
||||
timestamp: props.timestamp,
|
||||
tags: props.tags,
|
||||
isPublic: props.isPublic,
|
||||
message: props.message,
|
||||
sessionId: props.sessionId,
|
||||
noteId: props.noteId
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCopy = () => {
|
||||
copy(`${window.location.origin}${session(props.sessionId)}${props.timestamp > 0 ? '?jumpto=' + props.timestamp : ''}`);
|
||||
copy(
|
||||
`${window.location.origin}${session(props.sessionId)}${
|
||||
props.timestamp > 0 ? '?jumpto=' + props.timestamp : ''
|
||||
}`
|
||||
);
|
||||
toast.success('Note URL copied to clipboard');
|
||||
};
|
||||
|
||||
|
|
@ -44,7 +63,7 @@ function NoteEvent(props: Props) {
|
|||
) {
|
||||
notesStore.deleteNote(props.noteId).then((r) => {
|
||||
props.filterOutNote(props.noteId);
|
||||
filterOutTimelineNote(props.noteId)
|
||||
filterOutTimelineNote(props.noteId);
|
||||
toast.success('Note deleted');
|
||||
});
|
||||
}
|
||||
|
|
@ -64,7 +83,7 @@ function NoteEvent(props: Props) {
|
|||
<Icon name="quotes" color="main" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div>{props.userId}</div>
|
||||
<div>{props.userEmail}</div>
|
||||
<div className="text-disabled-text">
|
||||
{formatTimeOrDate(props.date as unknown as number, timezone)}
|
||||
</div>
|
||||
|
|
@ -80,6 +99,7 @@ function NoteEvent(props: Props) {
|
|||
<div className="flex items-center gap-1">
|
||||
{props.tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
style={{ background: tagProps[tag], userSelect: 'none' }}
|
||||
className="rounded-xl text-sm px-2 py-1 text-white"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
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 { setCreateNoteTooltip, addNote, updateNote } 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 {
|
||||
isVisible: boolean;
|
||||
time: number;
|
||||
setCreateNoteTooltip: (state: any) => void;
|
||||
addNote: (note: Note) => void;
|
||||
updateNote: (note: Note) => void;
|
||||
sessionId: string;
|
||||
isEdit: string;
|
||||
editNote: WriteNote;
|
||||
}
|
||||
|
||||
function CreateNote({
|
||||
isVisible,
|
||||
time,
|
||||
setCreateNoteTooltip,
|
||||
sessionId,
|
||||
addNote,
|
||||
isEdit,
|
||||
editNote,
|
||||
updateNote,
|
||||
}: 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();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isEdit) {
|
||||
setTags(editNote.tags);
|
||||
setText(editNote.message);
|
||||
setPublic(editNote.isPublic);
|
||||
if (editNote.timestamp > 0) {
|
||||
setUseTs(true);
|
||||
}
|
||||
}
|
||||
}, [isEdit]);
|
||||
|
||||
const duration = Duration.fromMillis(time).toFormat('mm:ss');
|
||||
const stopEvents = (e: any) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
const note: WriteNote = {
|
||||
message: text,
|
||||
tags,
|
||||
timestamp: useTimestamp ? (isEdit ? editNote.timestamp : time) : -1,
|
||||
isPublic,
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
return notesStore.updateNote(editNote.noteId, note).then((r) => {
|
||||
toast.success('Note updated');
|
||||
notesStore.fetchSessionNotes(sessionId).then((notes) => {
|
||||
injectNotes(notes);
|
||||
updateNote(r);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error('Error updating note');
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
setCreateNoteTooltip({ isVisible: false, time: 0 });
|
||||
setText('');
|
||||
setTags([]);
|
||||
});
|
||||
}
|
||||
|
||||
return 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(() => {
|
||||
setCreateNoteTooltip({ isVisible: false, time: 0 });
|
||||
setText('');
|
||||
setTags([]);
|
||||
});
|
||||
};
|
||||
|
||||
const closeTooltip = () => {
|
||||
setCreateNoteTooltip({ 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: -260,
|
||||
width: 350,
|
||||
left: 'calc(50% - 175px)',
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}}
|
||||
onClick={stopEvents}
|
||||
>
|
||||
<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" onClick={() => setUseTs(!useTimestamp)}>
|
||||
<Checkbox checked={useTimestamp} />
|
||||
<span className="ml-1">{`at ${duration}`}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto cursor-pointer" onClick={closeTooltip}>
|
||||
<Icon name="close" size={20} />
|
||||
</div>
|
||||
</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">
|
||||
{TAGS.map((tag) => (
|
||||
<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">
|
||||
<Button variant="primary" className="mr-4" onClick={onSubmit}>
|
||||
Add Note
|
||||
</Button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
const {
|
||||
isVisible,
|
||||
time = 0,
|
||||
isEdit,
|
||||
note: editNote,
|
||||
// @ts-ignore
|
||||
} = state.getIn(['sessions', 'createNoteTooltip']);
|
||||
// @ts-ignore
|
||||
const sessionId = state.getIn(['sessions', 'current', 'sessionId']);
|
||||
return { isVisible, time, sessionId, isEdit, editNote };
|
||||
},
|
||||
{ setCreateNoteTooltip, addNote, updateNote }
|
||||
)(CreateNote);
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
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 {
|
||||
isVisible: boolean;
|
||||
time: number;
|
||||
setNoteTooltip: (state: any) => void
|
||||
addNote: (note: Note) => void
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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: -260,
|
||||
width: 350,
|
||||
left: 'calc(50% - 175px)',
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}}
|
||||
onClick={stopEvents}
|
||||
>
|
||||
<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" onClick={() => setUseTs(!useTimestamp)}>
|
||||
<Checkbox checked={useTimestamp} />
|
||||
<span className="ml-1">{`at ${duration}`}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto cursor-pointer" onClick={closeTooltip}>
|
||||
<Icon name="close" size={20} />
|
||||
</div>
|
||||
</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">
|
||||
{TAGS.map((tag) => (
|
||||
<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">
|
||||
<Button variant="primary" className="mr-4" onClick={onSubmit}>
|
||||
Add Note
|
||||
</Button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => {
|
||||
// @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);
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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';
|
||||
|
||||
interface Props {
|
||||
userEmail: number;
|
||||
timestamp: number;
|
||||
tags: iTag[];
|
||||
isPublic: boolean;
|
||||
message: string;
|
||||
sessionId: string;
|
||||
date: string;
|
||||
noteId: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ReadNote(props: Props) {
|
||||
const { settingsStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: '45%', left: 'calc(50% - 300px)' }}>
|
||||
<div
|
||||
className="flex items-start flex-col p-4 border gap-2"
|
||||
style={{ background: '#FFFEF5', width: 600 }}
|
||||
>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="p-2 bg-gray-light rounded-full">
|
||||
<Icon name="quotes" color="main" size={16} />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div>{props.userEmail}</div>
|
||||
<div className="text-disabled-text">
|
||||
{formatTimeOrDate(props.date as unknown as number, timezone)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto cursor-pointer" onClick={props.onClose}>
|
||||
<Icon name="close" size={18} />
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ReadNote);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import TimeTooltip from './TimeTooltip';
|
||||
import NoteTooltip from './NoteTooltip';
|
||||
import CreateNote from './CreateNote';
|
||||
import store from 'App/store';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ function TooltipContainer({ live }: { live: boolean }) {
|
|||
<Provider store={store}>
|
||||
<>
|
||||
<TimeTooltip liveTimeTravel={live} />
|
||||
<NoteTooltip />
|
||||
<CreateNote />
|
||||
</>
|
||||
</Provider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
|||
import { Icon } from 'UI'
|
||||
import { connectPlayer, pause } from 'Player';
|
||||
import { connect } from 'react-redux'
|
||||
import { setNoteTooltip } from 'Duck/sessions';
|
||||
import { setCreateNoteTooltip } from 'Duck/sessions';
|
||||
|
||||
function NotePopup({ setNoteTooltip, time }: { setNoteTooltip: (args: any) => void, time: number }) {
|
||||
function NotePopup({ setCreateNoteTooltip, time }: { setCreateNoteTooltip: (args: any) => void, time: number }) {
|
||||
const toggleNotePopup = () => {
|
||||
pause();
|
||||
setNoteTooltip({ time: time, isVisible: true })
|
||||
setCreateNoteTooltip({ time: time, isVisible: true })
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -28,7 +28,7 @@ const NotePopupPl = connectPlayer(
|
|||
)(React.memo(NotePopup));
|
||||
|
||||
const NotePopupComp = connect(
|
||||
null, { setNoteTooltip }
|
||||
null, { setCreateNoteTooltip }
|
||||
)(NotePopupPl)
|
||||
|
||||
export default React.memo(NotePopupComp)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ function NoteItem(props: Props) {
|
|||
<PlayLink
|
||||
isAssist={false}
|
||||
viewed={false}
|
||||
sessionId={props.sessionId + (props.timestamp > 0 ? '?jumpto=' + props.timestamp : '')}
|
||||
sessionId={props.sessionId + (props.timestamp > 0 ? `?jumpto=${props.timestamp}¬e=${props.noteId}` : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 cursor-pointer">
|
||||
|
|
|
|||
|
|
@ -1,82 +1,65 @@
|
|||
import React from 'react';
|
||||
import { NoContent, Pagination, Icon } from 'UI';
|
||||
import { NoContent, Pagination, Loader, Icon } from 'UI';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import NoteItem from './NoteItem';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
|
||||
function NotesList({ members }: {members: Array<Record<string, any>>}) {
|
||||
const { notesStore } = useStore()
|
||||
const [list, setList] = React.useState<Note[]>([])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!notesStore.notes.length) {
|
||||
notesStore.fetchNotes().then(notes => setList(notes))
|
||||
notesStore.fetchNotes()
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (notesStore.notes.length) {
|
||||
if (notesStore.activeTags.length) {
|
||||
const tagsLen = notesStore.activeTags.length
|
||||
const filteredList: Note[] = notesStore.notes.filter(note => {
|
||||
for (let i = 0; i < tagsLen; i++) {
|
||||
const tag = notesStore.activeTags[i]
|
||||
if (note.tags.includes(tag)) {
|
||||
return note
|
||||
}
|
||||
}
|
||||
})
|
||||
setList(filteredList)
|
||||
} else {
|
||||
setList(notesStore.notes)
|
||||
}
|
||||
}
|
||||
}, [notesStore.activeTags])
|
||||
const list = notesStore.notes
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Icon name="no-metrics" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="text-center text-gray-600 my-4">No notes yet</div>
|
||||
<Loader loading={notesStore.loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="text-center text-gray-600 my-4">No notes yet</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border-b rounded bg-white">
|
||||
{sliceListPerPage(list, notesStore.page - 1, notesStore.pageSize).map(note => (
|
||||
<React.Fragment key={note.noteId}>
|
||||
<NoteItem
|
||||
userId={note.userId}
|
||||
tags={note.tags}
|
||||
timestamp={note.timestamp}
|
||||
isPublic={note.isPublic}
|
||||
description={note.message}
|
||||
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="mt-3 border-b rounded bg-white">
|
||||
{sliceListPerPage(list, notesStore.page - 1, notesStore.pageSize).map(note => (
|
||||
<React.Fragment key={note.noteId}>
|
||||
<NoteItem
|
||||
userId={note.userId}
|
||||
tags={note.tags}
|
||||
timestamp={note.timestamp}
|
||||
isPublic={note.isPublic}
|
||||
description={note.message}
|
||||
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 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
|
||||
<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
|
||||
</div>
|
||||
<Pagination
|
||||
page={notesStore.page}
|
||||
totalPages={Math.ceil(list.length / notesStore.pageSize)}
|
||||
onPageChange={(page) => notesStore.changePage(page)}
|
||||
limit={notesStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
page={notesStore.page}
|
||||
totalPages={Math.ceil(list.length / notesStore.pageSize)}
|
||||
onPageChange={(page) => notesStore.changePage(page)}
|
||||
limit={notesStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import React from 'react';
|
||||
import Select from 'Shared/Select';
|
||||
import { TAGS, iTag } from 'App/services/NotesService';
|
||||
import { TagItem } from '../SessionTags';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const sortOptionsMap = {
|
||||
'createdAt-DESC': 'Newest',
|
||||
'createdAt-ASC': 'Oldest',
|
||||
};
|
||||
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
|
||||
|
||||
function NoteTags() {
|
||||
const { notesStore } = useStore()
|
||||
const defaultOption = sortOptions[0].value;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
|
|
@ -18,6 +26,8 @@ function NoteTags() {
|
|||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Select name="sortSessions" plain right options={sortOptions} onChange={({ value }) => notesStore.toggleSort(value.value)} defaultValue={defaultOption} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,12 @@ 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 SET_CREATE_NOTE_TOOLTIP = 'sessions/SET_CREATE_NOTE_TOOLTIP'
|
||||
const SET_EDIT_NOTE_TOOLTIP = 'sessions/SET_CREATE_NOTE_TOOLTIP'
|
||||
const FILTER_OUT_NOTE = 'sessions/FILTER_OUT_NOTE'
|
||||
const ADD_NOTE = 'sessions/ADD_NOTE'
|
||||
const UPDATE_NOTE = 'sessions/UPDATE_NOTE'
|
||||
|
||||
const SET_SESSION_PATH = 'sessions/SET_SESSION_PATH';
|
||||
const LAST_PLAYED_SESSION_ID = `${name}/LAST_PLAYED_SESSION_ID`;
|
||||
|
|
@ -67,7 +70,7 @@ const initialState = Map({
|
|||
sessionPath: {},
|
||||
lastPlayedSessionId: null,
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false },
|
||||
noteTooltip: { time: 0, isVisible: false },
|
||||
createNoteTooltip: { time: 0, isVisible: false, isEdit: false, note: null },
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
|
|
@ -196,14 +199,15 @@ 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 SET_CREATE_NOTE_TOOLTIP:
|
||||
return state.set('createNoteTooltip', action.noteTooltip);
|
||||
case SET_EDIT_NOTE_TOOLTIP:
|
||||
return state.set('createNoteTooltip', 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
|
||||
|
|
@ -212,6 +216,9 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return aTs - bTs
|
||||
})
|
||||
)
|
||||
case UPDATE_NOTE:
|
||||
const index = state.getIn(['current', 'notesWithEvents']).findIndex(item => item.noteId === action.note.noteId)
|
||||
return state.setIn(['current', 'notesWithEvents', index], action.note)
|
||||
case SET_SESSION_PATH:
|
||||
return state.set('sessionPath', action.path);
|
||||
case LAST_PLAYED_SESSION_ID:
|
||||
|
|
@ -382,9 +389,16 @@ export function setTimelineHoverTime(timeLineTooltip) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setNoteTooltip(noteTooltip) {
|
||||
export function setCreateNoteTooltip(noteTooltip) {
|
||||
return {
|
||||
type: SET_NOTE_TOOLTIP,
|
||||
type: SET_CREATE_NOTE_TOOLTIP,
|
||||
noteTooltip
|
||||
}
|
||||
}
|
||||
|
||||
export function setEditNoteTooltip(noteTooltip) {
|
||||
return {
|
||||
type: SET_EDIT_NOTE_TOOLTIP,
|
||||
noteTooltip
|
||||
}
|
||||
}
|
||||
|
|
@ -403,6 +417,13 @@ export function addNote(note) {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateNote(note) {
|
||||
return {
|
||||
type: UPDATE_NOTE,
|
||||
note
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionPath(path) {
|
||||
return {
|
||||
type: SET_SESSION_PATH,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default class NotesStore {
|
|||
page = 1
|
||||
pageSize = 15
|
||||
activeTags: iTag[] = []
|
||||
sort = ''
|
||||
sort = 'createdAt'
|
||||
order: 'DESC' | 'ASC' = 'DESC'
|
||||
|
||||
constructor() {
|
||||
|
|
@ -23,7 +23,7 @@ export default class NotesStore {
|
|||
async fetchNotes() {
|
||||
const filter: NotesFilter = {
|
||||
page: this.page,
|
||||
limit: 15,
|
||||
limit: this.pageSize,
|
||||
sort: this.sort,
|
||||
order: this.order,
|
||||
tags: this.activeTags,
|
||||
|
|
@ -78,6 +78,24 @@ export default class NotesStore {
|
|||
}
|
||||
}
|
||||
|
||||
async updateNote(noteId: string, note: WriteNote) {
|
||||
this.loading = true
|
||||
try {
|
||||
const updated = await notesService.updateNote(noteId, note)
|
||||
return updated
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
getNoteById(noteId: number, notes?: Note[]) {
|
||||
const notesSource = notes ? notes : this.notes
|
||||
|
||||
return notesSource.find(note => note.noteId === noteId)
|
||||
}
|
||||
|
||||
changePage(page: number) {
|
||||
this.page = page
|
||||
}
|
||||
|
|
@ -85,8 +103,18 @@ export default class NotesStore {
|
|||
toggleTag(tag: iTag) {
|
||||
if (this.activeTags.includes(tag)) {
|
||||
this.activeTags = this.activeTags.filter(exTag => tag !== exTag)
|
||||
this.fetchNotes()
|
||||
} else {
|
||||
this.activeTags = [...this.activeTags, tag]
|
||||
this.fetchNotes()
|
||||
}
|
||||
}
|
||||
|
||||
toggleSort(sort: string) {
|
||||
const sortOrder = sort.split('-')[1]
|
||||
// @ts-ignore
|
||||
this.order = sortOrder
|
||||
|
||||
this.fetchNotes()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default class NotesService {
|
|||
})
|
||||
}
|
||||
|
||||
updateNote(noteID: string, note: Note): Promise<Note> {
|
||||
updateNote(noteID: string, note: WriteNote): Promise<Note> {
|
||||
return this.client.post(`/notes/${noteID}`, note)
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue