Skip to content
Open
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
195 changes: 161 additions & 34 deletions src/components/map-projects/MapProject.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const MapProject = () => {
const queryParams = new URLSearchParams(location.search)
return queryParams.get('templateFrom')
}, [location.search])
const AUTOSAVE_DELAY_MS = 5000

const bridgeRef = React.useRef()
const facetsRequestsRef = React.useRef({})
Expand Down Expand Up @@ -229,6 +230,11 @@ const MapProject = () => {
const [columnWidth, setColumnWidth] = React.useState({})
const [logs, setLogs] = React.useState({})
const [projectLogs, setProjectLogs] = React.useState([])
const logsRef = React.useRef({})
const projectLogsRef = React.useRef([])
const autosaveTimerRef = React.useRef(null)
const autosaveReasonsRef = React.useRef([])
const configSnapshotOnOpenRef = React.useRef(null)
const [filterModel, setFilterModel] = React.useState({ items: [] });
const [filterPanelAnchorEl, setFilterPanelAnchorEl] = React.useState(null)
const [retired, setRetired] = React.useState(false)
Expand All @@ -250,6 +256,19 @@ const MapProject = () => {
setAlert(false)
}, [])

React.useEffect(() => {
logsRef.current = logs
}, [logs])

React.useEffect(() => {
projectLogsRef.current = projectLogs
}, [projectLogs])

React.useEffect(() => () => {
if(autosaveTimerRef.current)
clearTimeout(autosaveTimerRef.current)
}, [])

// repo state
const [repo, setRepo] = React.useState(false)
const [repoVersion, setRepoVersion] = React.useState(false)
Expand Down Expand Up @@ -666,8 +685,12 @@ const MapProject = () => {
setFilters(response.data?.filters || {})
if(response.data?.url) {
APIService.new().overrideURL(response.data.url).appendToUrl('logs/').get().then(response => {
setLogs(response.data.logs?.row_logs || [])
setProjectLogs(response.data.logs?.project_logs || [])
const rowLogs = response.data.logs?.row_logs || []
const savedProjectLogs = response.data.logs?.project_logs || []
logsRef.current = rowLogs
projectLogsRef.current = savedProjectLogs
setLogs(rowLogs)
setProjectLogs(savedProjectLogs)
projectLog({action: 'Opened'})
})
}
Expand Down Expand Up @@ -1365,7 +1388,17 @@ const MapProject = () => {
})
}

const onSave = () => {
const onSave = (options = {}) => {
const saveOptions = options?.preventDefault ? {} : options
const saveSource = saveOptions.source || 'manual'
const isAutoSave = saveSource === 'auto'
if(isAutoSave && !project?.id)
return
if(!isAutoSave && autosaveTimerRef.current) {
clearTimeout(autosaveTimerRef.current)
autosaveTimerRef.current = null
autosaveReasonsRef.current = []
}
if(!repoVersion?.version_url || !selectedTargetRepoVersion) {
setConfigure(true)
setAlert({
Expand All @@ -1378,6 +1411,8 @@ const MapProject = () => {
})
return
}
const rowLogsForSave = saveOptions.logs || logsRef.current
const projectLogsForSave = saveOptions.projectLogs || projectLogsRef.current
setIsSaving(true)
const f = getFileObjectFromRows()
const selected = map(mapSelected, (data, i) => {
Expand Down Expand Up @@ -1459,30 +1494,70 @@ const MapProject = () => {
service.then(response => {
setIsSaving(false)
if(response?.data?.id) {
projectLog({action: isUpdate ? 'Updated' : 'Created', extras: isUpdate ? undefined : {project: response.data}})
setConfigure(false)
const saveLog = {
action: isAutoSave ? 'Auto Saved' : (isUpdate ? 'Updated' : 'Created'),
description: isAutoSave ? t('map_project.auto_saved_changes') : undefined,
created_at: moment().toDate(),
user: user.username || user.id,
extras: isAutoSave ? {reasons: saveOptions.reasons || []} : (isUpdate ? undefined : {project: response.data})
}
const savedProjectLogs = [saveLog, ...projectLogsForSave]
projectLogsRef.current = savedProjectLogs
setProjectLogs(savedProjectLogs)
if(saveOptions.closeConfigure !== false) {
configSnapshotOnOpenRef.current = null
setConfigure(false)
}
setProjectPromptTemplateKey(response.data?.prompt_template_key || getProjectPromptTemplateKey())
setProject(response.data)
if(response.data.url)
history.push(response.data.url)
baseSetAlert({severity: 'success', message: t('map_project.successfully_saved'), duration: 2000})
if(!isAutoSave)
baseSetAlert({severity: 'success', message: t('map_project.successfully_saved'), duration: 2000})

APIService.new().overrideURL(response.data.url).appendToUrl('logs/').post({logs: {row_logs: logs, project_logs: projectLogs}}).then(() => {})
APIService.new().overrideURL(response.data.url).appendToUrl('logs/').post({logs: {row_logs: rowLogsForSave, project_logs: savedProjectLogs}}).then(() => {})
}
})
}).finally(() => setIsSaving(false))
}

const log = (data, index) => {
let idx = index === undefined ? rowIndex : index
setLogs(prev => ({...prev, [idx]: [{created_at: moment().toDate(), user: user.username || user.id, ...data}, ...(prev[idx] || [])]}))
const nextLogs = {...logsRef.current, [idx]: [{created_at: moment().toDate(), user: user.username || user.id, ...data}, ...(logsRef.current[idx] || [])]}
logsRef.current = nextLogs
setLogs(nextLogs)
}

const projectLog = data => {
const newLog = {...data, created_at: moment().toDate(), user: user.username || user.id}
const newLogs = [newLog, ...projectLogs]
setProjectLogs(prev => [newLog, ...prev])
const newLogs = [newLog, ...projectLogsRef.current]
projectLogsRef.current = newLogs
setProjectLogs(newLogs)
if(project?.url)
APIService.new().overrideURL(project.url).appendToUrl('logs/').post({logs: {row_logs: logs, project_logs: newLogs}}).then(() => {})
APIService.new().overrideURL(project.url).appendToUrl('logs/').post({logs: {row_logs: logsRef.current, project_logs: newLogs}}).then(() => {})
}

const scheduleAutoSave = reason => {
if(!project?.id)
return

autosaveReasonsRef.current = uniq([...autosaveReasonsRef.current, reason])
if(autosaveTimerRef.current)
clearTimeout(autosaveTimerRef.current)

autosaveTimerRef.current = setTimeout(() => {
autosaveTimerRef.current = null
if(!project?.id) {
autosaveReasonsRef.current = []
return
}
if(isSaving) {
scheduleAutoSave(reason)
return
}
const reasons = autosaveReasonsRef.current
autosaveReasonsRef.current = []
onSave({source: 'auto', closeConfigure: false, reasons})
}, AUTOSAVE_DELAY_MS)
}

// ── ocl_online#105 Phase 5: AutomatchRun attribution ──────────────────────
Expand Down Expand Up @@ -1619,6 +1694,53 @@ const MapProject = () => {
return includeDefaultFilter ? allFilters : omit(allFilters, Object.keys(defaultFilters))
}

const getConfigurationSnapshot = () => JSON.stringify({
owner,
name,
description,
repo_url: repo?.url,
repo_version_url: repoVersion?.version_url,
algorithms: map(algosSelected, algo => omit(algo, ['__key'])),
score_configuration: candidatesScore,
lookup_config: lookupConfig,
namespace,
encoder_model: encoderModel || DEFAULT_ENCODER_MODEL,
include_retired: retired,
filters: getFilters(),
prompt_template_key: getProjectPromptTemplateKey(),
prompt_output_locale: promptOutputLocale || '',
input_locale: inputLocale || '',
use_lexical_variants: Boolean(useLexicalVariants),
columns: map(columns, col => ({
dataKey: col.dataKey,
mapped: col.mapped,
hidden: columnVisibilityModel[col.dataKey] === false,
width: columnWidth[col.dataKey] || undefined,
ai_assistant_hidden: AIAssistantColumns[col.dataKey] === false
}))
})

const closeConfiguration = () => {
const openedSnapshot = configSnapshotOnOpenRef.current
const changed = openedSnapshot && openedSnapshot !== getConfigurationSnapshot()
setConfigure(false)
configSnapshotOnOpenRef.current = null
if(changed)
scheduleAutoSave('configuration_change')
}

const setConfigureWithAutosave = nextConfigure => {
if(nextConfigure)
setConfigure(true)
else
closeConfiguration()
}

React.useEffect(() => {
if(configure && !configSnapshotOnOpenRef.current)
configSnapshotOnOpenRef.current = getConfigurationSnapshot()
}, [configure])

const getPayloadForMatching = (rows, _repo, _filters) => {
return {
rows: map(rows, row => prepareRow(row)),
Expand Down Expand Up @@ -1984,6 +2106,8 @@ const MapProject = () => {
} : {})
}
})
if(!abortRef.current)
scheduleAutoSave('auto_match')
} finally {
await completeAutomatchRun(_selectedAlgos, rowsToProcess)
}
Expand Down Expand Up @@ -2644,7 +2768,7 @@ const MapProject = () => {
conceptCacheRef.current = next
setConceptCache(next)
})
setConfigure(false)
closeConfiguration()
setShowProjectLogs(false)
setRow(csvRow)
setSearchStr(getRowNameValue(csvRow) || '')
Expand Down Expand Up @@ -2721,6 +2845,7 @@ const MapProject = () => {
const newRowStatuses = {...rowStatuses, reviewed: uniq([...rowStatuses.reviewed, rowIndex]), readyForReview: without(rowStatuses.readyForReview, rowIndex), unmapped: without(rowStatuses.unmapped, rowIndex)}
setRowStatuses(newRowStatuses)
log({'action': 'approved'})
scheduleAutoSave('decision_change')
if(next){
const nextRow = data[selectedRowStatus === 'all' ? rowIndex + 1 : find(rowStatuses[selectedRowStatus], idx => idx > rowIndex)]
if(nextRow !== undefined)
Expand Down Expand Up @@ -2813,6 +2938,7 @@ const MapProject = () => {
})
if(newValue !== 'map' && !logged)
log({action: newValue || 'decision_changed', description: t('map_project.decision_changed_to_none'), extras: newValue ? {} : {decision: t('map_project.none')}})
scheduleAutoSave('decision_change')
}

const getBulkActionLabel = action => {
Expand All @@ -2828,25 +2954,24 @@ const MapProject = () => {

const addBulkLogs = (indexes, action, description, extras = {}) => {
const createdAt = moment().toDate()
setLogs(prev => {
const next = {...prev}
indexes.forEach(index => {
const resolvedDescription = typeof description === 'function' ? description(index) : description
const resolvedExtras = typeof extras === 'function' ? extras(index) : extras
next[index] = [{
created_at: createdAt,
user: user.username || user.id,
action,
description: resolvedDescription,
extras: {
bulk_origin: 'mapper-left-panel-selection',
bulk_action: action,
...resolvedExtras
}
}, ...(next[index] || [])]
})
return next
const next = {...logsRef.current}
indexes.forEach(index => {
const resolvedDescription = typeof description === 'function' ? description(index) : description
const resolvedExtras = typeof extras === 'function' ? extras(index) : extras
next[index] = [{
created_at: createdAt,
user: user.username || user.id,
action,
description: resolvedDescription,
extras: {
bulk_origin: 'mapper-left-panel-selection',
bulk_action: action,
...resolvedExtras
}
}, ...(next[index] || [])]
})
logsRef.current = next
setLogs(next)
}

const getSelectedRowIndexes = (_rows = data) => {
Expand Down Expand Up @@ -3006,6 +3131,8 @@ const MapProject = () => {
duration: 4,
message: t('map_project.bulk_action_summary', {changed: changed.length, skipped: skipped.length})
})
if(changed.length)
scheduleAutoSave('decision_change')
clearBulkSelection()
}

Expand Down Expand Up @@ -4688,7 +4815,7 @@ const MapProject = () => {
isValidColumnValue={isValidColumnValue}
updateColumn={updateColumn}
configure={configure}
setConfigure={setConfigure}
setConfigure={setConfigureWithAutosave}
columnVisibilityModel={columnVisibilityModel}
setColumnVisibilityModel={setColumnVisibilityModel}
onSave={onSave}
Expand Down Expand Up @@ -4895,14 +5022,14 @@ const MapProject = () => {
onProjectLogsClick={() => {
const newValue = !showProjectLogs
if(newValue) {
setConfigure(false)
closeConfiguration()
onCloseDecisions()
}
setShowProjectLogs(newValue)
}}
isProjectsLogOpen={showProjectLogs}
configure={configure}
setConfigure={setConfigure}
setConfigure={setConfigureWithAutosave}
onCopyClick={onCopyClick}
/>
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/map-projects/ProjectLogs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const ProjectLogs = ({onClose, logs, project}) => {
return [<i key="created" className="fa-brands fa-octopus-deploy" style={{color: PRIMARY_COLORS.main, fontSize: '1.25rem', width: '1em', height: '1em'}}></i>, 'primary']
if(action === 'updated')
return [<HistoryIcon key='update' color='warning' />, 'warning']
if(action === 'auto saved')
return [<HistoryIcon key='auto-save' color='primary' />, 'primary']
if(action === 'saved_to_collection')
return [<RepoIcon key='collection' selected noTooltip />, 'primary']
if(action === 'opened')
Expand Down Expand Up @@ -101,6 +103,8 @@ const ProjectLogs = ({onClose, logs, project}) => {
/>
}

if(log?.action?.toLowerCase() === 'auto saved')
return log.description || t('map_project.auto_saved_changes')
if(log.description)
return log.description
return startCase(log.action)
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@
"sort_by_unified_score": "Unified Score",
"project_logs": "Project History",
"project_logs_tooltip": "Project History",
"auto_saved_changes": "Auto saved changes",
"saved_to_collection": "Saved to collection",
"bridge_terminology_search": "Bridge terminology search<0>Premium</0>",
"bridge_terminology_search_description": "Include mappings in the <0>CIEL Interface Terminology</0> to identify additional high quality candidates. Only available for compatible target repositories and matching algorithms.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@
"sort_by_unified_score": "Puntuación Unificada",
"project_logs": "Registros del proyecto",
"project_logs_tooltip": "Ver registros del proyecto",
"auto_saved_changes": "Cambios guardados automáticamente",
"saved_to_collection": "Guardado en la colección",
"bridge_terminology_search": "Búsqueda de terminología Bridge<0>Premium</0>",
"bridge_terminology_search_description": "Incluir mapeos en la <0>Terminología de Interfaz CIEL</0> para identificar candidatos adicionales de alta calidad. Disponible solo para repositorios de destino y algoritmos de coincidencia compatibles.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@
"sort_by_unified_score": "按统一分数排序",
"project_logs": "项目日志",
"project_logs_tooltip": "查看项目日志",
"auto_saved_changes": "已自动保存更改",
"saved_to_collection": "已保存到集合",
"bridge_terminology_search": "Bridge 术语搜索<0>高级版</0>",
"bridge_terminology_search_description": "包含 <0>CIEL 接口术语</0> 中的映射,以识别更多高质量候选项。仅适用于兼容的目标仓库和匹配算法。",
Expand Down