MediaWiki:Common.js: Difference between revisions
OCR: person cards clarify scan vs wiki page vs team update |
OCR roster table: who / wiki page / what changes on that profile |
||
| Line 1,551: | Line 1,551: | ||
var grouped = groupFactsByPerson(proposed); | var grouped = groupFactsByPerson(proposed); | ||
var lastGroup = null; | var lastGroup = null; | ||
var | var $table = $('<table>', { 'class': 'ocr-roster-table' }); | ||
var $thead = $('<thead>').append($('<tr>').append( | |||
$('<th>', { 'class': 'ocr-col-do' }).text('Write?'), | |||
$('<th>').text('Person on the scan'), | |||
$('<th>').text('Wiki soldier page'), | |||
$('<th>').text('What changes on that page') | |||
)); | |||
var $tbody = $('<tbody>'); | |||
$table.append($thead, $tbody); | |||
grouped.order.forEach(function (key) { | grouped.order.forEach(function (key) { | ||
| Line 1,567: | Line 1,575: | ||
}); | }); | ||
var teamFact = teamFacts[0]; | var teamFact = teamFacts[0]; | ||
var teamDone = teamFact && teamFact.already_on_page; | var teamDone = !!(teamFact && teamFact.already_on_page); | ||
var teamReady = teamFact && !teamFact.already_on_page && | var teamReady = !!(teamFact && !teamFact.already_on_page && (teamFact.value || '').trim()); | ||
var included = !!(title && teamReady && teamFact.selected !== false); | var included = !!(title && teamReady && teamFact.selected !== false); | ||
var teamLabel = (teamFact && (teamFact.value || '').trim()) | |||
? ('IPW ' + String(teamFact.value).replace(/^IPW\s+/i, '')) | |||
: (meta.team || 'this team'); | |||
if (groupId && groupId !== lastGroup) { | if (groupId && groupId !== lastGroup) { | ||
| Line 1,579: | Line 1,590: | ||
return (grouped.byPerson[k][0] || {}).group_id === groupId; | return (grouped.byPerson[k][0] || {}).group_id === groupId; | ||
}).length; | }).length; | ||
$ | $tbody.append($('<tr>', { 'class': 'ocr-group-heading-row' }).append( | ||
$('<td>', { colspan: 4 }).text( | |||
groupLabel + ' — ' + inGroup + ' people on this document' | |||
) | |||
)); | )); | ||
} | } | ||
var $card = $('<tr>', { | |||
var $card = $('< | |||
'class': 'ocr-person-card' + | 'class': 'ocr-person-card' + | ||
(title ? ' ocr-person-matched' : ' ocr-person-unresolved') + | (title ? ' ocr-person-matched' : ' ocr-person-unresolved') + | ||
| Line 1,592: | Line 1,604: | ||
}); | }); | ||
var $include = $('<input>', { | var $include = $('<input>', { | ||
type: 'checkbox', | type: 'checkbox', | ||
'class': 'ocr-include', | 'class': 'ocr-include', | ||
checked: included | checked: included, | ||
title: teamDone ? 'This team is already on their profile' : 'Write the team onto their soldier page' | |||
}); | }); | ||
if (teamDone || !teamFact) { | |||
$include.prop('disabled', true).prop('checked', false); | |||
} | |||
var $tgtShared = $('<input>', { | var $tgtShared = $('<input>', { | ||
type: 'text', | type: 'text', | ||
'class': 'ocr-person-title', | 'class': 'ocr-person-title', | ||
value: title, | value: title, | ||
placeholder: ' | placeholder: 'Search surname or ASN to match a page…' | ||
}); | }); | ||
var $suggest = $('<div>', { 'class': 'ocr-suggest' }).hide(); | var $suggest = $('<div>', { 'class': 'ocr-suggest' }).hide(); | ||
var $pageLink = $('<div>', { 'class': 'ocr-wiki-page-name' }); | |||
var $matchStatus = $('<div>', { 'class': 'ocr-match-status' }); | var $matchStatus = $('<div>', { 'class': 'ocr-match-status' }); | ||
function setMatch(t) { | var $outcome = $('<div>', { 'class': 'ocr-row-outcome' }); | ||
function syncPrimaryChecks() { | |||
var on = $include.prop('checked') && !!$.trim($tgtShared.val() || '') && !teamDone; | |||
$card.find('.ocr-fact-primary.ocr-fact-writable input[type="checkbox"]').prop('checked', on); | |||
if (!on) { | |||
$card.find('.ocr-fact-secondary.ocr-fact-writable input[type="checkbox"]').prop('checked', false); | |||
} | |||
} | |||
function refreshOutcome() { | |||
var page = $.trim($tgtShared.val() || ''); | |||
var extra = []; | |||
$card.find('.ocr-fact-secondary.ocr-fact-writable').each(function () { | |||
var $row = $(this); | |||
if ($row.find('input[type="checkbox"]').first().prop('checked')) { | |||
extra.push($row.find('.ocr-change-line').text()); | |||
} | |||
}); | |||
$outcome.removeClass( | |||
'ocr-outcome-write ocr-outcome-skip ocr-outcome-unmatched ocr-outcome-idle' | |||
); | |||
if (!page) { | |||
$include.prop('checked', false).prop('disabled', true); | |||
syncPrimaryChecks(); | |||
$outcome.addClass('ocr-outcome-unmatched'); | |||
$outcome.text( | |||
'No wiki page matched. Nothing will be written until you search and pick their soldier page' + | |||
(teamFact && teamFact.value ? (', then Team ' + teamLabel + ' can be added.') : '.') | |||
); | |||
return; | |||
} | |||
if (teamDone && !extra.length) { | |||
$include.prop('disabled', true).prop('checked', false); | |||
$outcome.addClass('ocr-outcome-skip'); | |||
$outcome.empty().append( | |||
document.createTextNode('Already on their profile: Team ' + teamLabel + '. Nothing new will be written.') | |||
); | |||
return; | |||
} | |||
$include.prop('disabled', false); | |||
if ($include.prop('checked') || extra.length) { | |||
$outcome.addClass('ocr-outcome-write'); | |||
var $ul = $('<ul>', { 'class': 'ocr-row-write-list' }); | |||
if ($include.prop('checked') && teamReady) { | |||
$ul.append($('<li>').text('Set Team to ' + teamLabel + ' on that soldier page')); | |||
} | |||
extra.forEach(function (line) { | |||
$ul.append($('<li>').text(line)); | |||
}); | |||
$outcome.empty().append($ul); | |||
return; | |||
} | |||
$outcome.addClass('ocr-outcome-idle'); | |||
$outcome.text( | |||
'Matched, but not selected. Check “Write?” to add Team ' + teamLabel + ' to that soldier page.' | |||
); | |||
} | |||
function setMatch(t, silent) { | |||
title = t || ''; | title = t || ''; | ||
$tgtShared.val(title); | $tgtShared.val(title); | ||
$card.find('.ocr-fact-target').val(title); | $card.find('.ocr-fact-target').val(title); | ||
$pageLink.empty(); | |||
if (title) { | if (title) { | ||
$card.removeClass('ocr-person-unresolved').addClass('ocr-person-matched'); | $card.removeClass('ocr-person-unresolved').addClass('ocr-person-matched'); | ||
$ | $pageLink.append($('<a>', { | ||
href: mw.util.getUrl(title), | |||
text: title, | |||
); | target: '_blank' | ||
})); | |||
$matchStatus.text('Open the page in a new tab to see their current profile.'); | |||
} else { | } else { | ||
$card.removeClass('ocr-person-matched').addClass('ocr-person-unresolved'); | $card.removeClass('ocr-person-matched').addClass('ocr-person-unresolved'); | ||
$matchStatus.text(' | $pageLink.append($('<span>', { 'class': 'ocr-wiki-missing' }).text('Not matched to a soldier page')); | ||
$matchStatus.text('Type a surname or ASN below, then pick the page.'); | |||
$include.prop('checked', false); | $include.prop('checked', false); | ||
syncPrimaryChecks(); | syncPrimaryChecks(); | ||
} | } | ||
if (onChange) { | refreshOutcome(); | ||
if (!silent && onChange) { | |||
onChange(); | onChange(); | ||
} | } | ||
} | } | ||
var $do = $('<td>', { 'class': 'ocr-col-do' }); | |||
$do.append($include); | |||
var $who = $('<td>', { 'class': 'ocr-col-who' }); | |||
$who.append($('<div>', { 'class': 'ocr-scan-name' }).text(personDisplayName(first))); | |||
var whoBits = []; | |||
if (meta.rank) { | |||
whoBits.push(meta.rank); | |||
} | |||
if (meta.role) { | |||
whoBits.push(meta.role); | |||
} | |||
if (meta.asn) { | |||
whoBits.push('ASN ' + meta.asn); | |||
} | } | ||
$ | if (whoBits.length) { | ||
$who.append($('<div>', { 'class': 'ocr-scan-bits' }).text( | |||
'From the scan (not written): ' + whoBits.join(' · ') | |||
)); | |||
} | } | ||
$ | |||
$ | var $page = $('<td>', { 'class': 'ocr-col-page' }); | ||
$page.append($pageLink, $matchStatus, $tgtShared, $suggest); | |||
bindSoldierTypeahead($tgtShared, $suggest, setMatch); | bindSoldierTypeahead($tgtShared, $suggest, setMatch); | ||
$tgtShared.on('change', function () { | $tgtShared.on('change', function () { | ||
| Line 1,693: | Line 1,752: | ||
} | } | ||
}); | }); | ||
$ | $page.append($pick); | ||
} | } | ||
var $action = $('<td>', { 'class': 'ocr-col-action' }); | |||
var $ | $action.append($outcome); | ||
$ | |||
var $ | var $store = $('<div>', { 'class': 'ocr-fact-store' }); | ||
if (teamFact) { | if (teamFact) { | ||
var $teamRow = renderChangeRow(teamFact, job, { | var $teamRow = renderChangeRow(teamFact, job, { | ||
plainSentence: true, | plainSentence: true, | ||
forceSelected: included && !teamDone | forceSelected: included && !teamDone | ||
}); | }); | ||
$teamRow.find('.ocr-fact-target').val(title); | $teamRow.find('.ocr-fact-target').val(title); | ||
$teamRow.find('input[type="checkbox"]').first().addClass('ocr-primary-check'). | $teamRow.find('input[type="checkbox"]').first().addClass('ocr-primary-check'); | ||
$ | $store.append($teamRow); | ||
var | } | ||
$ | notes.forEach(function (fact) { | ||
var $nrow = renderChangeRow(fact, job, {}); | |||
$nrow.find('.ocr-fact-target').val(title); | |||
$ | $store.append($nrow); | ||
}); | |||
$action.append($store); | |||
if (secondary.length) { | |||
var $moreFields = $('<details>', { 'class': 'ocr-card-more' }); | |||
$moreFields.append($('<summary>').text( | |||
'Also write rank / ASN / role (' + secondary.length + ') — off unless you check them' | |||
)); | |||
secondary.forEach(function (fact) { | |||
var $row = renderChangeRow(fact, job, { forceSelected: false }); | |||
$row.find('.ocr-fact-target').val(title); | |||
$moreFields.append($row); | |||
}); | |||
$action.append($moreFields); | |||
} | } | ||
$include.on('change', function () { | $include.on('change', function () { | ||
if ($include.prop('checked') && !$.trim($tgtShared.val() || '')) { | if ($include.prop('checked') && !$.trim($tgtShared.val() || '')) { | ||
$include.prop('checked', false); | $include.prop('checked', false); | ||
$matchStatus.text('Match a soldier page first, then check | $matchStatus.text('Match a soldier page first, then check Write?.'); | ||
refreshOutcome(); | |||
return; | return; | ||
} | } | ||
if ($include.prop('checked') && teamDone) { | if ($include.prop('checked') && teamDone) { | ||
$include.prop('checked', false); | $include.prop('checked', false); | ||
refreshOutcome(); | |||
return; | return; | ||
} | } | ||
syncPrimaryChecks(); | syncPrimaryChecks(); | ||
refreshOutcome(); | |||
if (onChange) { | if (onChange) { | ||
onChange(); | onChange(); | ||
| Line 1,738: | Line 1,807: | ||
}); | }); | ||
$card.append($do, $who, $page, $action); | |||
$card.on('change', 'input[type="checkbox"], input.ocr-fact-value', function (ev) { | $card.on('change', 'input[type="checkbox"], input.ocr-fact-value', function (ev) { | ||
if ($(ev.target).hasClass('ocr-include')) { | if ($(ev.target).hasClass('ocr-include')) { | ||
return; | return; | ||
} | } | ||
refreshOutcome(); | |||
if (onChange) { | if (onChange) { | ||
onChange(); | onChange(); | ||
} | } | ||
}); | }); | ||
$ | $tbody.append($card); | ||
setMatch(title, true); | |||
}); | }); | ||
$list.append($table); | |||
} | } | ||
| Line 1,873: | Line 1,919: | ||
function renderReview($host, job, cfg) { | function renderReview($host, job, cfg) { | ||
$host.empty(); | $host.empty(); | ||
var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-review-v2' }); | var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-review-v2' + (job.multi_person ? ' ocr-review-roster' : '') }); | ||
$panel.append($('<p>').append( | if (job.multi_person) { | ||
$panel.append($('<p>').append( | |||
$('<strong>').text('Each row is one person'), | |||
document.createTextNode( | |||
' — the last column is the only thing that would be written on a soldier page. Rank, role, and ASN on the left are from the scan (for matching). Nothing is saved until Apply.' | |||
) | |||
) | )); | ||
} else { | |||
$panel.append($('<p>').append( | |||
$('<strong>').text('Confirm changes, then Apply'), | |||
document.createTextNode(' — nothing is saved until you click Apply.') | |||
)); | |||
} | |||
if (job.multi_person && job.people_summary) { | if (job.multi_person && job.people_summary) { | ||
var ps = job.people_summary; | var ps = job.people_summary; | ||
$ | var $banner = $('<div>', { 'class': 'ocr-outcome-banner' }); | ||
(ps.detected || 0) + ' people on this document | $banner.append($('<strong>').text( | ||
(ps.detected || 0) + ' people on this document' | |||
)); | )); | ||
var bannerBits = []; | |||
bannerBits.push((ps.matched || 0) + ' matched to a wiki page'); | |||
bannerBits.push((ps.unresolved || 0) + ' not matched yet'); | |||
if (ps.already_on_page) { | |||
bannerBits.push((ps.already_on_page) + ' already have this team on their profile (nothing to write)'); | |||
} | |||
if (ps.inferred_teams && ps.inferred_teams.length) { | |||
bannerBits.push('teams ' + ps.inferred_teams.join(', ')); | |||
} else if (ps.teams && ps.teams.length) { | |||
bannerBits.push('teams ' + ps.teams.join(', ')); | |||
} | |||
$banner.append($('<div>').text(bannerBits.join(' · '))); | |||
$panel.append($banner); | |||
} else { | } else { | ||
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | $panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | ||
| Line 1,942: | Line 2,002: | ||
var $previewBox = $('<div>', { 'class': 'ocr-apply-preview' }); | var $previewBox = $('<div>', { 'class': 'ocr-apply-preview' }); | ||
$previewBox.append($('<strong>').text(' | $previewBox.append($('<strong>').text('Soldier pages that will be edited')); | ||
$previewBox.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | $previewBox.append($('<p>', { 'class': 'ocr-fact-meta ocr-preview-hint' }).text( | ||
job.multi_person | job.multi_person | ||
? ' | ? 'Grouped by wiki page. Rank/role/ASN from the scan are not listed here unless you check them under a row.' | ||
: 'Checked items below will be written on Apply.' | : 'Checked items below will be written on Apply.' | ||
)); | )); | ||
var $previewCount = $('<p>', { 'class': 'ocr-preview-count' }); | |||
var $previewUl = $('<ul>', { 'class': 'ocr-apply-preview-list' }); | var $previewUl = $('<ul>', { 'class': 'ocr-apply-preview-list' }); | ||
$previewBox.append($previewUl); | $previewBox.append($previewCount, $previewUl); | ||
function refreshPreview() { | function refreshPreview() { | ||
var props = collectProposed(); | var props = collectProposed(); | ||
var | var byPage = {}; | ||
props.forEach(function (p) { | props.forEach(function (p) { | ||
if (!p.selected || !isWritableFact(p) || p.already_on_page) { | if (!p.selected || !isWritableFact(p) || p.already_on_page) { | ||
| Line 1,962: | Line 2,023: | ||
return; | return; | ||
} | } | ||
if (!byPage[page]) { | |||
byPage[page] = []; | |||
} | |||
byPage[page].push(p); | |||
}); | }); | ||
var pages = Object.keys(byPage); | |||
$previewUl.empty(); | $previewUl.empty(); | ||
if (! | if (!pages.length) { | ||
$previewCount.text(''); | |||
$previewUl.append($('<li>', { 'class': 'ocr-fact-meta' }).text( | $previewUl.append($('<li>', { 'class': 'ocr-fact-meta' }).text( | ||
job.multi_person | job.multi_person | ||
? 'Nothing | ? 'Nothing will be written on Apply. Check Write? on a row (after matching a wiki page) to add that person.' | ||
: 'Nothing selected yet.' | : 'Nothing selected yet.' | ||
)); | )); | ||
} else { | } else { | ||
$previewCount.text( | |||
$ | 'Apply will edit ' + pages.length + ' soldier page' + (pages.length === 1 ? '' : 's') + ':' | ||
); | |||
pages.forEach(function (page) { | |||
var $li = $('<li>', { 'class': 'ocr-preview-person' }); | |||
$li.append($('<a>', { href: mw.util.getUrl(page), text: page, target: '_blank' })); | |||
var $sub = $('<ul>'); | |||
byPage[page].forEach(function (p) { | |||
$sub.append($('<li>').text(profileUpdateSentence(p))); | |||
}); | |||
$li.append($sub); | |||
$previewUl.append($li); | |||
}); | }); | ||
} | } | ||
} | } | ||
$facts.append($previewBox); | |||
if (job.multi_person) { | if (job.multi_person) { | ||
var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' }); | var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' }); | ||
var $selMatched = $('<button>', { type: 'button', text: ' | var $selMatched = $('<button>', { type: 'button', text: 'Write team for every matched person who still needs it' }); | ||
var $deselUnres = $('<button>', { type: 'button', text: 'Clear unmatched' }); | var $deselUnres = $('<button>', { type: 'button', text: 'Clear unmatched' }); | ||
$toolbar.append($selMatched, $deselUnres); | $toolbar.append($selMatched, $deselUnres); | ||
| Line 1,989: | Line 2,065: | ||
$list.find('.ocr-person-matched:not(.ocr-person-done)').each(function () { | $list.find('.ocr-person-matched:not(.ocr-person-done)').each(function () { | ||
var $card = $(this); | var $card = $(this); | ||
$card.find('.ocr-include').prop('checked', true).trigger('change'); | $card.find('.ocr-include').prop('disabled', false).prop('checked', true).trigger('change'); | ||
}); | }); | ||
refreshPreview(); | refreshPreview(); | ||
| Line 2,001: | Line 2,077: | ||
} | } | ||
$facts.append($list); | $facts.append($list); | ||
/* Advanced: OCR text, doc type, gallery, overwrite, rebuild */ | /* Advanced: OCR text, doc type, gallery, overwrite, rebuild */ | ||
| Line 2,187: | Line 2,262: | ||
$apply.on('click', function () { | $apply.on('click', function () { | ||
if (!job.identity_ok && !$forceId.find('input').prop('checked')) { | if (!job.multi_person && !job.identity_ok && !$forceId.find('input').prop('checked')) { | ||
$status.text('Confirm identity (Advanced → checkbox) before Apply, or fix the wrong document.'); | $status.text('Confirm identity (Advanced → checkbox) before Apply, or fix the wrong document.'); | ||
$adv.prop('open', true); | $adv.prop('open', true); | ||
| Line 2,366: | Line 2,441: | ||
$up.append($('<h3>').text('Upload a document')); | $up.append($('<h3>').text('Upload a document')); | ||
$up.append($('<p>').text( | $up.append($('<p>').text( | ||
'For | 'For a roster, review is a table: who is on the scan, which wiki page, and what would be written on that profile (usually Team). Rank, role, and ASN are matching cues, not automatic edits.' | ||
)); | )); | ||
var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | ||