Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/map-projects/MapProject.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ import { createLookupSession, LOOKUP_NOT_FOUND } from './lookupSession'
import { parseConceptKey } from './conceptKey'
import { normalizeLookupConfig } from './lookupConfigFields'
import { getDeclaredLocales, getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getSavedTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo'
import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, findTargetRowView, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs, withCachedConcept, withCandidateNames, withOwnNames, withSavedMatch } from './viewBuilders.js'
import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, findTargetRowView, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs, withCachedConcept, withCandidateNames, buildMatchEntries, restoredNote, withOwnNames, withSavedMatch } from './viewBuilders.js'
import { formatClockTime, getCapacityWaitLabel, mergeCapacityWaits } from './rowProgress.js'

import './MapProject.scss'
Expand Down Expand Up @@ -1563,7 +1563,7 @@ const MapProject = () => {
_states[state] = _states[state] || []
_states[state].push(index)
_decisions[index] = (data['__Decision__'] || data['__row_decision__']) === t('map_project.none') ? undefined : (data['__Decision__'] || data['__row_decision__'])
_notes[index] = data['__Note__']
_notes[index] = restoredNote(data['__Note__'], savedMatches[index])
_mapTypes[index] = data['__Map Type__'] || data['__map_type__']
_proposed[index] = (data['__Proposed__'] || data['__proposed__']) ? JSON.parse(data['__Proposed__'] || data['__proposed__']) : undefined
data = omit(data, [...reservedKeys, ...optionalReservedKeys])
Expand Down Expand Up @@ -1717,7 +1717,7 @@ const MapProject = () => {
const projectLogsForSave = options.projectLogs || projectLogsRef.current
setIsSaving(true)
const f = getFileObjectFromRows()
const selected = map(mapSelected, (data, i) => {
const selected = buildMatchEntries(map(data, row => row.__index), mapSelected, notes, (data, i) => {
return {
url: data?.url,
repoURL: data?.repo?.version_url || data?.repo?.url,
Expand Down
31 changes: 30 additions & 1 deletion src/components/map-projects/__tests__/views.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {
withCachedConcept,
withCandidateNames,
withOwnNames,
withSavedMatch
withSavedMatch,
buildMatchEntries,
restoredNote
} from '../viewBuilders.js'

// ---------- Helpers ----------
Expand Down Expand Up @@ -831,3 +833,30 @@ test('withOwnNames: a mapped target keeps its own names and highlight over a can
assert.equal(withOwnNames(withNames, searchedTarget).names, glucoseNames)
assert.deepEqual(withOwnNames(withNames, searchedTarget).search_meta.search_highlight, { name: ['x'] })
})

// ---------- unmapped row notes (OpenConceptLab/ocl_issues#2984) ----------

// The save's entry for a row `mapSelected` holds, as onSave builds it.
const entryFor = (target, index) => ({rowIndex: index, url: target?.url, note: notesFixture[index] || undefined})
let notesFixture = {}

// Save, serialise, reopen: what MapProject sends and what it restores.
const roundTrip = (mapSelected, notes, rows) => {
notesFixture = notes
const sent = JSON.parse(JSON.stringify(buildMatchEntries(rows, mapSelected, notes, entryFor)))
const saved = Object.fromEntries(sent.map(entry => [entry.rowIndex, entry]))
return {sent, restored: Object.fromEntries(rows.map(index => [index, restoredNote(undefined, saved[index])]))}
}

test('a note on an unmapped row survives save and reopen', () => {
const { sent, restored } = roundTrip({0: searchedTarget}, {0: 'mapped note', 1: 'needs a clinician', 2: ''}, [0, 1, 2])
assert.deepEqual(sent.map(entry => entry.rowIndex), ['0', 1])
assert.deepEqual(restored, {0: 'mapped note', 1: 'needs a clinician', 2: undefined})
assert.equal(restoredNote('csv note', sent[1]), 'csv note')
})

test('a cleared row (null in mapSelected) with a note is sent once and keeps its note', () => {
const { sent, restored } = roundTrip({0: null}, {0: 'rejected: wrong unit'}, [0])
assert.equal(sent.length, 1)
assert.equal(restored[0], 'rejected: wrong unit')
})
19 changes: 19 additions & 0 deletions src/components/map-projects/viewBuilders.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,25 @@ export const withSavedMatch = (target, savedMatch) => {
}
}

/**
* The `matches` a save sends: one entry per row. `entryFor` builds the entry
* of each row `mapSelected` holds, a cleared one (a null) included. A row it
* holds no key for, with a review note, gets a note-only entry, as the note is
* kept only in its entry (OpenConceptLab/ocl_issues#2984).
*/
export const buildMatchEntries = (rowIndexes, mapSelected, notes, entryFor) => [
...Object.entries(mapSelected).map(([index, target]) => entryFor(target, index)),
...rowIndexes
.filter(index => !Object.prototype.hasOwnProperty.call(mapSelected, index) && notes[index])
.map(index => ({rowIndex: index, note: notes[index]}))
]

/**
* A row's review note on reopen: the one its CSV row holds, else the one its
* saved `matches` entry holds (OpenConceptLab/ocl_issues#2984).
*/
export const restoredNote = (csvNote, savedMatch) => csvNote || savedMatch?.note || undefined

/**
* A mapped target with its own names and match highlight put back over a
* candidate's: the ones it was mapped with, or that a reopened project
Expand Down
Loading