Skip to content

Commit 7df2e21

Browse files
committed
OpenConceptLab/ocl_issues#2190 | Project discuss/logs
1 parent 944f9ba commit 7df2e21

4 files changed

Lines changed: 189 additions & 10 deletions

File tree

src/components/map-projects/ColumnMapTable.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import find from 'lodash/find';
1414
import map from 'lodash/map';
1515
import omit from 'lodash/omit';
1616
import get from 'lodash/get'
17+
import orderBy from 'lodash/orderBy'
1718

1819

1920
const HeaderAutocomplete = ({headers, isValid, ...rest}) => {
@@ -91,7 +92,7 @@ const TargetSourceAutoComplete = ({sources, possibleValue, selected, onChange, .
9192
'.MuiButtonBase-root': {color: '#000'}
9293
}}
9394
renderInput={(params) => <TextField margin='dense' size='small' {...params} />}
94-
options={sources}
95+
options={orderBy(sources, 'id')}
9596
value={selected ? find(sources, {url: selected}) || '' : ''}
9697
onChange={(event, val) => onChange(val?.url)}
9798
{...rest}
@@ -146,7 +147,7 @@ const ColumnMapTable = ({validColumns, columns, isValid, onUpdate, sx, setColumn
146147
map(targetSourcesFromRows[column.dataKey], (target, index) => {
147148
return (
148149
<div key={index} style={{display: 'flex', alignItems: 'center'}}>
149-
<span style={{marginRight: '8px', textTransform: 'uppercase'}}>
150+
<span style={{marginRight: '8px'}}>
150151
{target}:
151152
</span>
152153
<TargetSourceAutoComplete
@@ -164,7 +165,7 @@ const ColumnMapTable = ({validColumns, columns, isValid, onUpdate, sx, setColumn
164165
{
165166
'Mapping: Code' === column.label &&
166167
<div style={{display: 'flex', alignItems: 'center'}}>
167-
<span style={{marginRight: '8px', textTransform: 'uppercase'}}>
168+
<span style={{marginRight: '8px'}}>
168169
{targetSourcesFromRows[column.dataKey]}:
169170
</span>
170171
<TargetSourceAutoComplete
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from 'react'
2+
import moment from 'moment'
3+
4+
import Timeline from '@mui/lab/Timeline';
5+
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
6+
import TimelineSeparator from '@mui/lab/TimelineSeparator';
7+
import TimelineConnector from '@mui/lab/TimelineConnector';
8+
import TimelineContent from '@mui/lab/TimelineContent';
9+
import TimelineDot from '@mui/lab/TimelineDot';
10+
import Typography from '@mui/material/Typography';
11+
import Tooltip from '@mui/material/Tooltip';
12+
import TextField from '@mui/material/TextField';
13+
import Button from '@mui/material/Button'
14+
import CommentIcon from '@mui/icons-material/Message';
15+
import MapIcon from '@mui/icons-material/Link';
16+
import UnmapIcon from '@mui/icons-material/LinkOff';
17+
import RejectIcon from '@mui/icons-material/Clear';
18+
import AutoMatchIcon from '@mui/icons-material/MotionPhotosAutoOutlined';
19+
20+
import map from 'lodash/map'
21+
import startCase from 'lodash/startCase'
22+
import orderBy from 'lodash/orderBy'
23+
24+
25+
const Discuss = ({ logs, onAdd }) => {
26+
const [comment, setComment] = React.useState('')
27+
const getTitle = log => {
28+
if(['mapped', 'unmapped', 'auto-matched'].includes(log.action)) {
29+
return `${startCase(log.action)}: ${log.extras.name} (${log.extras.map_type || log.extras.mapType})`
30+
}
31+
if(['commented'].includes(log.action)) {
32+
return <b>{log.description}</b>
33+
}
34+
return log.description || startCase(log.action)
35+
}
36+
37+
const getIcon = (log, color) => {
38+
if(log.action === 'commented')
39+
return <CommentIcon fontSize='small' color={color} />
40+
if(['mapped'].includes(log.action))
41+
return <MapIcon fontSize='small' color={color} />
42+
if(['auto-matched'].includes(log.action))
43+
return <AutoMatchIcon fontSize='small' color={color} />
44+
if(log.action === 'unmapped')
45+
return <UnmapIcon fontSize='small' color={color} />
46+
if(log.action === 'rejected')
47+
return <RejectIcon fontSize='small' color={color} />
48+
return log.user.slice(0, 2).toUpperCase()
49+
}
50+
51+
const getDotColor = log => {
52+
if(['unmapped', 'rejected'].includes(log.action))
53+
return 'error'
54+
if(['proposed'].includes(log.action))
55+
return 'warning'
56+
if(['mapped', 'auto-matched'].includes(log.action))
57+
return 'primary'
58+
return undefined
59+
}
60+
61+
const onClick = () => {
62+
onAdd(comment)
63+
setComment('')
64+
}
65+
66+
67+
return (
68+
<>
69+
<Timeline
70+
sx={{
71+
maxHeight: 'calc(100vh - 560px)',
72+
overflow: 'auto',
73+
p: 2,
74+
paddingTop: '4px',
75+
marginBottom: 0,
76+
[`& .${timelineItemClasses.root}:before`]: {
77+
flex: 0,
78+
padding: 0,
79+
},
80+
}}
81+
>
82+
{
83+
map(orderBy(logs, log => moment(log.created_at), ['desc']), (log, index) => {
84+
const dotColor = getDotColor(log)
85+
return (
86+
<TimelineItem key={log.created_at.toString()}>
87+
<TimelineSeparator>
88+
<Tooltip title={startCase(log.action)}>
89+
<TimelineDot color={dotColor} variant="outlined">
90+
{getIcon(log, dotColor)}
91+
</TimelineDot>
92+
</Tooltip>
93+
{
94+
index !== (logs.length - 1) &&
95+
<TimelineConnector />
96+
}
97+
</TimelineSeparator>
98+
<TimelineContent>
99+
<Typography component="span" sx={{fontSize: '14px'}}>
100+
{getTitle(log)}
101+
</Typography>
102+
<Typography sx={{fontSize: '12px', color: 'rgba(0, 0, 0, 0.7)'}}>{log.user} at {moment(log.created_at).format('ll LTS')}</Typography>
103+
</TimelineContent>
104+
</TimelineItem>
105+
)
106+
})
107+
}
108+
</Timeline>
109+
<div className='col-xs-12' style={{padding: '16px'}}>
110+
<TextField
111+
autoFocus
112+
fullWidth
113+
multiline
114+
minRows={2}
115+
maxRows={4}
116+
value={comment}
117+
label='Comment'
118+
onChange={event => setComment(event.target.value || '')}
119+
sx={{
120+
'.MuiInputBase-root': {
121+
fontSize: '14px',
122+
padding: '4px 8px',
123+
'.MuiInputBase-inputMultiline': {
124+
height: '20px'
125+
}
126+
}
127+
}}
128+
/>
129+
<Button onClick={onClick} sx={{textTransform: 'none', marginTop: '8px'}} variant='outlined' color='primary'>
130+
Comment
131+
</Button>
132+
</div>
133+
</>
134+
)
135+
}
136+
137+
export default Discuss;

src/components/map-projects/MapProject.jsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,20 @@ import ReviewNote from './ReviewNote'
9191
import Propose from './Propose'
9292
import Candidates from './Candidates'
9393
import SearchCandidates from './SearchCandidates'
94+
import Discuss from './Discuss'
9495

9596
import './MapProject.scss'
9697
import '../common/ResizablePanel.scss'
9798

99+
100+
// const LOG = {
101+
// action: '',
102+
// user: '',
103+
// description: '',
104+
// extras: {},
105+
// created_at: ''
106+
// }
107+
98108
const MapProject = () => {
99109
const { toggles, setAlert: baseSetAlert } = React.useContext(OperationsContext);
100110
const user = getCurrentUser()
@@ -149,6 +159,7 @@ const MapProject = () => {
149159
const [alert, setAlert] = React.useState(false)
150160
const [columnVisibilityModel, setColumnVisibilityModel] = React.useState({})
151161
const [columnWidth, setColumnWidth] = React.useState({})
162+
const [logs, setLogs] = React.useState({})
152163

153164
// repo state
154165
const [repo, setRepo] = React.useState(false)
@@ -187,6 +198,9 @@ const MapProject = () => {
187198
const fetchAndSetProject = () => {
188199
let url = ['', params.ownerType, params.owner, 'map-projects', params.projectId, ''].join('/')
189200
APIService.new().overrideURL(url).get().then(response => {
201+
if(response.data.url) {
202+
APIService.new().overrideURL(response.data.url).appendToUrl('logs/').get().then(response => setLogs(response.data.logs || []))
203+
}
190204
if(response.data?.file_url) {
191205
fetch(response.data.file_url).then(res => res.text()).then(csvText => {
192206
const workbook = XLSX.read(csvText, { type: "string" });
@@ -491,10 +505,16 @@ const MapProject = () => {
491505
if(response.data.url)
492506
history.push(response.data.url)
493507
baseSetAlert({severity: 'success', message: 'Succesffully Saved.', duration: 2000})
508+
APIService.new().overrideURL(response.data.url).appendToUrl('logs/').post({logs: logs}).then(() => {})
494509
}
495510
})
496511
}
497512

513+
const log = (data, index) => {
514+
let idx = index === undefined ? rowIndex : index
515+
setLogs(prev => ({...prev, [idx]: [{...data, created_at: moment().toDate(), user: user.username || user.id}, ...(prev[idx] || [])]}))
516+
}
517+
498518
const fetchRepo = (url, _repo) => APIService.new().overrideURL(url).get().then(response => setRepo(response.data?.id ? response.data : _repo))
499519

500520
const fetchMappedSources = url => APIService.new().overrideURL(url).appendToUrl('mapped-sources/?excludeSelf=false&brief=true').get().then(response => setMappedSources(response?.data || []))
@@ -581,13 +601,15 @@ const MapProject = () => {
581601
setRowStatuses(prev => {
582602
forEach(data, concept => {
583603
if(get(concept, 'results.0.search_meta.match_type') === 'very_high') {
604+
let _concept = {...concept.results[0], repo: {..._repo, version: repoVersion?.id || _repo.version, version_url: repoVersion?.version_url || _repo.version_url}}
584605
setMapSelected(_prev => {
585-
_prev[concept.row.__index] = {...concept.results[0], repo: {..._repo, version: repoVersion?.id || _repo.version, version_url: repoVersion?.version_url || _repo.version_url}}
606+
_prev[concept.row.__index] = _concept
586607
return _prev
587608
})
588609
prev.readyForReview = uniq([...prev.readyForReview, concept.row.__index])
589610
setDecisions(prev => ({...prev, [concept.row.__index]: 'map'}))
590611
setMapTypes(prev => ({...prev, [concept.row.__index]: 'SAME-AS'}))
612+
log({action: 'auto-matched', extras: {repoVersion: repoVersion?.version_url || _repo.version_url, name: getConceptLabel(_concept), map_type: 'SAME-AS'}}, concept.row.__index)
591613
} else
592614
prev.unmapped = uniq([...prev.unmapped, concept.row.__index])
593615
})
@@ -610,7 +632,6 @@ const MapProject = () => {
610632
prev.readyForReview = []
611633
return prev
612634
})
613-
614635
await processWithConcurrency(repo);
615636
setEndMatchingAt(moment())
616637
setLoadingMatches(false)
@@ -851,7 +872,7 @@ const MapProject = () => {
851872
const onMap = (event, concept, unmap=false, mapType='SAME-AS', closeConcept=false) => {
852873
event.preventDefault()
853874
event.stopPropagation()
854-
_onMap(concept, unmap)
875+
_onMap(concept, unmap, mapType)
855876
setRowStatuses(prev => {
856877
prev.reviewed = without(prev.reviewed, rowIndex)
857878
if(unmap) {
@@ -871,9 +892,12 @@ const MapProject = () => {
871892
return false
872893
}
873894

874-
const _onMap = (concept, unmap=false) => {
895+
const _onMap = (concept, unmap=false, mapType='SAME-AS') => {
875896
setMapSelected(prev => ({...prev, [rowIndex]: unmap ? null : {...concept, repo: {...repo, version: repoVersion?.id || repo.version, version_url: repoVersion?.version_url || repo.version_url}}}))
876897
setDecisions(prev => ({...prev, [rowIndex]: unmap ? null : 'map'}))
898+
if(concept?.url)
899+
log({action: unmap ? 'unmapped' : 'mapped', extras: {object_url: concept?.url, map_type: mapType, name: getConceptLabel(concept)}})
900+
877901
}
878902

879903
const onReviewDone = (next = false) => {
@@ -884,6 +908,7 @@ const MapProject = () => {
884908
const nextRow = data[selectedRowStatus === 'all' ? rowIndex + 1 : find(rowStatuses[selectedRowStatus], idx => idx > rowIndex)]
885909
if(nextRow !== undefined)
886910
setTimeout(() => onCSVRowSelect(nextRow), 300)
911+
log({'action': 'approved'})
887912
}
888913
}
889914

@@ -921,14 +946,21 @@ const MapProject = () => {
921946
}
922947

923948
const onDecisionChange = (event, newValue) => {
949+
let logged = false
924950
if(newValue === 'rejected') {
925951
let selected = mapSelected[rowIndex]
926952
selected = getConcept(selected) || selected
927953
if(selected?.id) {
928-
let comment = `Rejected ${getConceptLabel(selected)}`
954+
let conceptLabel = getConceptLabel(selected)
955+
let comment = `Rejected ${conceptLabel}`
929956
if(notes[rowIndex])
930957
comment = notes[rowIndex] + '\n' + comment
931958
setNotes({...notes, [rowIndex]: comment})
959+
log({action: newValue, description: comment, extras: {object_url: selected?.url, name: conceptLabel}})
960+
logged = true
961+
} else {
962+
log({action: newValue})
963+
logged = true
932964
}
933965
}
934966
if(newValue !== 'map')
@@ -937,8 +969,11 @@ const MapProject = () => {
937969
setProposed(prev => ({...prev, [rowIndex]: undefined}))
938970

939971
setDecisions(prev => ({...prev, [rowIndex]: newValue || undefined}))
940-
if(newValue === 'propose')
972+
if(newValue === 'propose') {
941973
setAlert({message: 'Proposed successfully.', duration: 2, severity: 'success'})
974+
log({action: 'proposed'})
975+
logged = true
976+
}
942977

943978
setRowStatuses(prev => {
944979
prev.reviewed = without(prev.reviewed, rowIndex)
@@ -952,6 +987,8 @@ const MapProject = () => {
952987
updateMatchTypeCounts(null, prev)
953988
return prev
954989
})
990+
if(newValue !== 'map' && !logged)
991+
log({action: newValue || 'decision_changed', description: 'Desicion Changed to None', extras: newValue ? {} : {decision: 'None'}})
955992
}
956993

957994
const fetchOtherCandidates = (_row, offset=0) => {
@@ -1545,6 +1582,10 @@ const MapProject = () => {
15451582
onSearch={searchCandidates}
15461583
/>
15471584
}
1585+
{
1586+
decisionTab === 'discuss' && isSplitView &&
1587+
<Discuss logs={logs[rowIndex]} onAdd={comment => comment ? log({action: 'commented', description: comment}) : null} />
1588+
}
15481589
</div>
15491590
<SearchHighlightsDialog
15501591
open={Boolean(showHighlights)}

src/components/map-projects/constants.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ export const MATCH_TYPES = {
7878
},
7979
}
8080

81-
export const DECISION_TABS = ['candidates', 'propose', 'search']
81+
export const DECISION_TABS = ['candidates', 'propose', 'search', 'discuss']

0 commit comments

Comments
 (0)