- }
- render() {
- let htmlBody
- let htmlHead = this.getHeader()
- if (this.thereAreSubCategories) {
- htmlBody = this.getBodyWithSubcategories()
- } else {
- htmlBody = this.getBody()
- }
- return (
-
- {htmlHead}
- {htmlBody}
+ return
{rows}
+ }, [
+ lqaNestedCategories,
+ severities,
+ getCategorySeverities,
+ getIssuesForCategoryWithSubcategory,
+ ])
+
+ const hasKudos = categoriesGroups.some(
+ (group) => group[0].get('label') === 'Kudos',
+ )
+
+ const kudosCount = hasKudos
+ ? qualitySummary
+ .get('revise_issues')
+ .find((item) => item.get('name') === 'Kudos')
+ ?.get('founds')
+ ?.get('Neutral') || 0
+ : 0
+
+ return (
+ <>
+
+ {renderHeader()}
+ {thereAreSubCategories ? renderBodyWithSubcategories() : renderBody()}
- )
- }
+ {hasKudos && (
+
+ )}
+ >
+ )
}
export default QualitySummaryTable
diff --git a/public/js/components/quality_report/QualitySummaryTable.test.js b/public/js/components/quality_report/QualitySummaryTable.test.js
new file mode 100644
index 0000000000..86215b2090
--- /dev/null
+++ b/public/js/components/quality_report/QualitySummaryTable.test.js
@@ -0,0 +1,356 @@
+import React from 'react'
+import {render, screen} from '@testing-library/react'
+import Immutable from 'immutable'
+
+import QualitySummaryTable from './QualitySummaryTable'
+
+// Helper to build an Immutable qualitySummary prop from plain JS
+const buildQualitySummary = (overrides = {}) => {
+ const defaults = {
+ revision_number: 1,
+ feedback: null,
+ score: 0,
+ quality_overall: 'excellent',
+ total_issues_weight: 0,
+ total_reviewed_words_count: 100,
+ passfail: '',
+ categories: [
+ {
+ label: 'Typing',
+ id: 1,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ {
+ label: 'Translation',
+ id: 2,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ {
+ label: 'Style',
+ id: 3,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ ],
+ revise_issues: {},
+ }
+
+ return Immutable.fromJS({...defaults, ...overrides})
+}
+
+const defaultJobInfo = Immutable.fromJS({
+ id: 123,
+ source: 'en-US',
+ target: 'it-IT',
+})
+
+const renderComponent = (qualitySummaryOverrides = {}) => {
+ const qualitySummary = buildQualitySummary(qualitySummaryOverrides)
+ return render(
+
,
+ )
+}
+
+describe('QualitySummaryTable', () => {
+ test('renders the header with category and severity columns', () => {
+ renderComponent()
+
+ expect(screen.getByText('Categories')).toBeInTheDocument()
+ expect(screen.getByText('Severities')).toBeInTheDocument()
+ expect(screen.getByText('Error Points')).toBeInTheDocument()
+ })
+
+ test('renders all category labels', () => {
+ renderComponent()
+
+ expect(screen.getByText('Typing')).toBeInTheDocument()
+ expect(screen.getByText('Translation')).toBeInTheDocument()
+ expect(screen.getByText('Style')).toBeInTheDocument()
+ })
+
+ test('renders total line', () => {
+ renderComponent()
+
+ expect(screen.getByText('Total')).toBeInTheDocument()
+ })
+
+ test('renders severity weight labels with penalty multiplier', () => {
+ renderComponent()
+
+ expect(screen.getByText('(x0.03)')).toBeInTheDocument()
+ expect(screen.getByText('(x1)')).toBeInTheDocument()
+ })
+
+ test('displays issue counts when revise_issues has data', () => {
+ renderComponent({
+ revise_issues: {
+ 1: {
+ name: 'Typing',
+ founds: {minor: 3, major: 1},
+ },
+ 2: {
+ name: 'Translation',
+ founds: {minor: 0, major: 2},
+ },
+ },
+ total_issues_weight: 2.09,
+ })
+
+ // Typing: minor=3, major=1 => error points = 3*0.03 + 1*1 = 1.09
+ expect(screen.getByText('3')).toBeInTheDocument()
+ // Translation: major=2 => error points = 2*1 = 2
+ expect(screen.getByText('2.09')).toBeInTheDocument()
+ })
+
+ test('renders zero total when no issues exist', () => {
+ renderComponent({total_issues_weight: 0})
+
+ const totalElements = screen.getAllByText('0')
+ expect(totalElements.length).toBeGreaterThan(0)
+ expect(screen.getByText('Total')).toBeInTheDocument()
+ })
+
+ test('renders with subcategories when present', () => {
+ // Note: getCategorySeverities checks `cat.get('severities')` for truthiness.
+ // An empty Immutable List is truthy, so parent categories should either omit
+ // severities or include the same severities as subcategories.
+ const categoriesData = Immutable.fromJS([
+ {
+ label: 'Accuracy',
+ id: 10,
+ severities: [
+ {label: 'minor', penalty: 1, sort: 1},
+ {label: 'major', penalty: 5, sort: 2},
+ ],
+ subcategories: [
+ {
+ label: 'Mistranslation',
+ id: 11,
+ severities: [
+ {label: 'minor', penalty: 1, sort: 1},
+ {label: 'major', penalty: 5, sort: 2},
+ ],
+ },
+ {
+ label: 'Omission',
+ id: 12,
+ severities: [
+ {label: 'minor', penalty: 1, sort: 1},
+ {label: 'major', penalty: 5, sort: 2},
+ ],
+ },
+ ],
+ options: [],
+ },
+ {
+ label: 'Fluency',
+ id: 20,
+ severities: [
+ {label: 'minor', penalty: 1, sort: 1},
+ {label: 'major', penalty: 5, sort: 2},
+ ],
+ subcategories: [
+ {
+ label: 'Grammar',
+ id: 21,
+ severities: [
+ {label: 'minor', penalty: 1, sort: 1},
+ {label: 'major', penalty: 5, sort: 2},
+ ],
+ },
+ ],
+ options: [],
+ },
+ ])
+
+ // Build revise_issues with integer keys so .get(sub.id) works
+ const reviseIssues = Immutable.Map().set(
+ 11,
+ Immutable.fromJS({name: 'Mistranslation', founds: {minor: 2, major: 1}}),
+ )
+
+ const qualitySummary = Immutable.fromJS({
+ revision_number: 1,
+ feedback: null,
+ score: 0,
+ quality_overall: 'excellent',
+ total_issues_weight: 7,
+ total_reviewed_words_count: 100,
+ passfail: '',
+ })
+ .set('categories', categoriesData)
+ .set('revise_issues', reviseIssues)
+
+ render(
+
,
+ )
+
+ // Parent categories should be displayed
+ expect(screen.getByText('Accuracy')).toBeInTheDocument()
+ expect(screen.getByText('Fluency')).toBeInTheDocument()
+ // Subcategory issues aggregated under Accuracy: minor=2, major=1
+ expect(screen.getByText('2')).toBeInTheDocument()
+ expect(screen.getByText('1')).toBeInTheDocument()
+ })
+
+ test('renders Kudos section when Kudos category exists', () => {
+ renderComponent({
+ categories: [
+ {
+ label: 'Typing',
+ id: 1,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ {
+ label: 'Kudos',
+ id: 99,
+ severities: [{label: 'Neutral', penalty: 0, sort: 0}],
+ subcategories: [],
+ options: [],
+ },
+ ],
+ revise_issues: {
+ 99: {name: 'Kudos', founds: {Neutral: 5}},
+ },
+ })
+
+ expect(screen.getByText('Kudos')).toBeInTheDocument()
+ expect(screen.getByText('5')).toBeInTheDocument()
+ })
+
+ test('renders Kudos with 0 when no neutral issues found', () => {
+ renderComponent({
+ categories: [
+ {
+ label: 'Typing',
+ id: 1,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ {
+ label: 'Kudos',
+ id: 99,
+ severities: [{label: 'Neutral', penalty: 0, sort: 0}],
+ subcategories: [],
+ options: [],
+ },
+ ],
+ revise_issues: {},
+ })
+
+ expect(screen.getByText('Kudos')).toBeInTheDocument()
+ // Kudos value should be 0 — find within the Kudos container
+ const kudosEl = screen.getByText('Kudos').closest('.qr-kudos')
+ expect(kudosEl).toHaveTextContent('0')
+ })
+
+ test('severities are sorted by sort property when defined', () => {
+ const qualitySummary = buildQualitySummary({
+ categories: [
+ {
+ label: 'Typing',
+ id: 1,
+ severities: [
+ {label: 'critical', penalty: 5, sort: 3},
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ ],
+ })
+
+ render(
+
,
+ )
+
+ // Verify the severity labels appear in sorted order (minor, major, critical)
+ const minorEl = screen.getByText('minor', {exact: false})
+ const majorEl = screen.getByText('major', {exact: false})
+ const criticalEl = screen.getByText('critical', {exact: false})
+
+ // Compare document order via compareDocumentPosition
+ expect(
+ minorEl.compareDocumentPosition(majorEl) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy()
+ expect(
+ majorEl.compareDocumentPosition(criticalEl) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy()
+ })
+
+ test('renders with different severity sets across categories', () => {
+ const qualitySummary = buildQualitySummary({
+ categories: [
+ {
+ label: 'Typing',
+ id: 1,
+ severities: [
+ {label: 'minor', penalty: 0.03, sort: 1},
+ {label: 'major', penalty: 1, sort: 2},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ {
+ label: 'Style',
+ id: 3,
+ severities: [
+ {label: 'minor', penalty: 0.1, sort: 1},
+ {label: 'critical', penalty: 5, sort: 3},
+ ],
+ subcategories: [],
+ options: [],
+ },
+ ],
+ })
+
+ render(
+
,
+ )
+
+ // Should render without errors even with different severity groups
+ expect(screen.getByText('Typing')).toBeInTheDocument()
+ expect(screen.getByText('Style')).toBeInTheDocument()
+ // Both severity groups should show penalty multipliers
+ expect(screen.getByText('(x0.03)')).toBeInTheDocument()
+ expect(screen.getByText('(x0.1)')).toBeInTheDocument()
+ })
+})
diff --git a/public/js/components/quality_report/SegmentQR.js b/public/js/components/quality_report/SegmentQR.js
index f9db22211c..f32fea02b1 100644
--- a/public/js/components/quality_report/SegmentQR.js
+++ b/public/js/components/quality_report/SegmentQR.js
@@ -1,6 +1,12 @@
-import React from 'react'
+import React, {
+ useState,
+ useMemo,
+ useRef,
+ useEffect,
+ useCallback,
+ createRef,
+} from 'react'
import classnames from 'classnames'
-import {isNull} from 'lodash/lang'
import TextUtils from '../../utils/textUtils'
import SegmentQRLine from './SegmentQRLine'
@@ -13,644 +19,581 @@ import DraftMatecatUtils from '../segments/utils/DraftMatecatUtils'
import SegmentQA from '../../../img/icons/SegmentQA'
import AlertIcon from '../../../img/icons/AlertIcon'
import InfoIcon from '../../../img/icons/InfoIcon'
+import {Badge, BADGE_MODE, BADGE_TYPE} from '../common/Badge'
+import Tooltip from '../common/Tooltip'
+import ReviseIssuesIcon from '../../../img/icons/ReviseIssuesIcon'
+import {Button, BUTTON_SIZE} from '../common/Button/Button'
+import ChevronDown from '../../../img/icons/ChevronDown'
+import ChevronUp from '../../../img/icons/ChevronUp'
-class SegmentQR extends React.Component {
- constructor(props) {
- super(props)
- this.source = this.props.segment.get('segment')
- this.suggestion = this.props.segment.get('suggestion')
- this.target =
- !isNull(this.props.segment.get('last_translation')) &&
- this.props.segment.get('last_translation')
- this.revise =
- !isNull(this.props.segment.get('last_revisions')) &&
- this.props.segment.get('last_revisions').find((value) => {
- return value.get('revision_number') === 1
- })
- this.revise2 =
- !isNull(this.props.segment.get('last_revisions')) &&
- this.props.segment.get('last_revisions').find((value) => {
- return value.get('revision_number') === 2
- })
-
- this.revise =
- this.revise && this.revise.size > 0
- ? this.revise.get('translation')
- : false
- this.revise2 =
- this.revise2 && this.revise2.size > 0
- ? this.revise2.get('translation')
- : false
- //If second pass separate the issues
- if (this.props.secondPassReviewEnabled) {
- this.issuesR1 = this.props.segment.get('issues').filter((value) => {
- return value.get('revision_number') === 1
- })
- this.issuesR2 = this.props.segment.get('issues').filter((value) => {
- return value.get('revision_number') === 2
- })
- }
+const QA_TYPES = ['ERROR', 'WARNING', 'INFO']
- this.state = {
- translateDiffOn:
- this.props.segment.get('last_translation') &&
- !isNull(this.props.segment.get('last_translation')) &&
- isNull(this.props.segment.get('last_revisions')),
- reviseDiffOn:
- !isNull(this.props.segment.get('last_revisions')) &&
- this.revise &&
- !this.revise2 &&
- this.props.segment.get('last_translation') &&
- !isNull(this.props.segment.get('last_translation')),
- revise2DiffOn:
- !isNull(this.props.segment.get('last_revisions')) &&
- this.revise2 &&
- (this.revise || !isNull(this.props.segment.get('last_translation'))),
- htmlDiff: '',
- automatedQaOpen:
- this.props.segment.get('issues').size === 0 &&
- this.props.segment.get('warnings').get('total') > 0,
- humanQaOpen:
- !this.props.secondPassReviewEnabled &&
- this.props.segment.get('issues').size > 0,
- r1QaOpen: this.props.revisionToShow === '1',
- r2QaOpen: this.props.revisionToShow === '2',
- }
- this.state.htmlDiff = this.initializeDiff()
- this.errorObj = {
- types: {
- TAGS: {
- label: 'Tag mismatch',
- },
- MISMATCH: {
- label: 'Character mismatch',
- },
- GLOSSARY: {
- label: 'Glossary',
- },
- },
- icons: {
- ERROR:
,
- WARNING:
,
- INFO:
,
- },
- }
- }
+const QA_CATEGORY_LABELS = {
+ TAGS: 'Tag mismatch',
+ MISMATCH: 'Character mismatch',
+ GLOSSARY: 'Glossary',
+}
- initializeDiff() {
- if (this.state.translateDiffOn) {
- return this.getDiffPatch(this.suggestion, this.target)
- } else if (this.state.reviseDiffOn) {
- let revise = this.revise
- return this.getDiffPatch(this.target, revise)
- } else if (this.state.revise2DiffOn) {
- let source = this.revise ? this.revise : this.target
- return this.getDiffPatch(source, this.revise2)
- }
- }
- openAutomatedQa() {
- this.setState({
- automatedQaOpen: true,
- humanQaOpen: false,
- r1QaOpen: false,
- r2QaOpen: false,
- })
- }
- openHumandQa() {
- this.setState({
- automatedQaOpen: false,
- humanQaOpen: true,
- })
- }
- openR1Qa() {
- this.setState({
- automatedQaOpen: false,
- r1QaOpen: true,
- r2QaOpen: false,
- })
+const QA_ICONS = {
+ ERROR:
,
+ WARNING:
,
+ INFO:
,
+}
+
+const strPadLeft = (value, pad, length) =>
+ (new Array(length + 1).join(pad) + value).slice(-length)
+
+const decodeTextAndTransformTags = (text, isRtl) => {
+ if (text) {
+ // Fix for more than 2 followed spaces
+ const normalized = text.replace(/ {2}/gi, ' ')
+ return DraftMatecatUtils.transformTagsToHtml(normalized, isRtl)
}
- openR2Qa() {
- this.setState({
- automatedQaOpen: false,
- r1QaOpen: false,
- r2QaOpen: true,
- })
+ return text
+}
+
+const getRevisionTranslation = (revisions, revisionNumber) => {
+ if (revisions === null) return false
+ const found = revisions.find(
+ (value) => value.get('revision_number') === revisionNumber,
+ )
+ return found && found.size > 0 ? found.get('translation') : false
+}
+
+const getStatusBadgeType = (status) => {
+ const upper = status.toUpperCase()
+ if (upper === SEGMENTS_STATUS.NEW || upper === SEGMENTS_STATUS.DRAFT) {
+ return BADGE_TYPE.GREY
}
- getAutomatedQaHtml() {
- let html = []
- let fnMap = (key, obj, type) => {
- let item = (
-
-
- {this.errorObj.icons[type]}
-
-
- {this.errorObj.types[key].label} ({obj.size})
-
-
- )
- html.push(item)
- }
- let details = this.props.segment
- .get('warnings')
- .get('details')
- .get('issues_info')
- if (details.get('ERROR').get('Categories').size > 0) {
- details
- .get('ERROR')
- .get('Categories')
- .entrySeq()
- .forEach((item) => {
- let key = item[0]
- let value = item[1]
- fnMap(key, value, 'ERROR')
- })
- }
- if (details.get('WARNING').get('Categories').size > 0) {
- details
- .get('WARNING')
- .get('Categories')
- .entrySeq()
- .forEach((item) => {
- let key = item[0]
- let value = item[1]
- fnMap(key, value, 'WARNING')
- })
- }
- if (details.get('INFO').get('Categories').size > 0) {
- details
- .get('INFO')
- .get('Categories')
- .entrySeq()
- .forEach((item) => {
- let key = item[0]
- let value = item[1]
- fnMap(key, value, 'INFO')
- })
+ if (upper === SEGMENTS_STATUS.APPROVED) return BADGE_TYPE.GREEN
+ if (upper === SEGMENTS_STATUS.APPROVED2) return BADGE_TYPE.PURPLE
+ return BADGE_TYPE.PRIMARY
+}
+
+function SegmentQR({segment, urls, secondPassReviewEnabled, revisionToShow}) {
+ // Derived values from props
+ const source = useMemo(() => segment.get('segment'), [segment])
+ const suggestion = useMemo(() => segment.get('suggestion'), [segment])
+ const target = useMemo(() => {
+ const t = segment.get('last_translation')
+ return t !== null && t
+ }, [segment])
+ const revise = useMemo(
+ () => getRevisionTranslation(segment.get('last_revisions'), 1),
+ [segment],
+ )
+ const revise2 = useMemo(
+ () => getRevisionTranslation(segment.get('last_revisions'), 2),
+ [segment],
+ )
+
+ // If second pass, separate the issues by revision number
+ const issuesR1 = useMemo(() => {
+ if (!secondPassReviewEnabled) return null
+ return segment
+ .get('issues')
+ .filter((value) => value.get('revision_number') === 1)
+ }, [segment, secondPassReviewEnabled])
+
+ const issuesR2 = useMemo(() => {
+ if (!secondPassReviewEnabled) return null
+ return segment
+ .get('issues')
+ .filter((value) => value.get('revision_number') === 2)
+ }, [segment, secondPassReviewEnabled])
+
+ // State
+ const lastTranslation = segment.get('last_translation')
+ const lastRevisions = segment.get('last_revisions')
+
+ const [translateDiffOn, setTranslateDiffOn] = useState(
+ lastTranslation !== null && lastTranslation && lastRevisions === null,
+ )
+ const [reviseDiffOn, setReviseDiffOn] = useState(
+ lastRevisions !== null &&
+ revise &&
+ !revise2 &&
+ lastTranslation !== null &&
+ !!lastTranslation,
+ )
+ const [revise2DiffOn, setRevise2DiffOn] = useState(
+ lastRevisions !== null && revise2 && (revise || lastTranslation !== null),
+ )
+ const [htmlDiff, setHtmlDiff] = useState('')
+ const [automatedQaOpen, setAutomatedQaOpen] = useState(
+ segment.get('issues').size === 0 &&
+ segment.get('warnings').get('total') > 0,
+ )
+ const [humanQaOpen, setHumanQaOpen] = useState(
+ !secondPassReviewEnabled && segment.get('issues').size > 0,
+ )
+ const [r1QaOpen, setR1QaOpen] = useState(revisionToShow === '1')
+ const [r2QaOpen, setR2QaOpen] = useState(revisionToShow === '2')
+ const [showHistory, setShowHistory] = useState(false)
+
+ const issuesContainer = useRef(null)
+
+ // Initialize diff on mount
+ useEffect(() => {
+ const getDiff = (src, tgt) => TextUtils.getDiffHtml(src, tgt)
+ if (translateDiffOn) {
+ setHtmlDiff(getDiff(suggestion, target))
+ } else if (reviseDiffOn) {
+ setHtmlDiff(getDiff(target, revise))
+ } else if (revise2DiffOn) {
+ setHtmlDiff(getDiff(revise || target, revise2))
}
+ // eslint-disable-next-line
+ }, [])
- return html
- }
- getHumanQaHtml(issues) {
- let html = []
- issues.map((issue, index) => {
- let item =
- html.push(item)
- })
+ useEffect(() => {
+ setR1QaOpen(revisionToShow === '1')
+ setR2QaOpen(revisionToShow === '2')
+ }, [revisionToShow])
- return html
- }
+ // QA tab handlers
+ const openAutomatedQa = useCallback(() => {
+ setAutomatedQaOpen(true)
+ setHumanQaOpen(false)
+ setR1QaOpen(false)
+ setR2QaOpen(false)
+ }, [])
+
+ const openHumanQa = useCallback(() => {
+ setAutomatedQaOpen(false)
+ setHumanQaOpen(true)
+ }, [])
+
+ const openR1Qa = useCallback(() => {
+ setAutomatedQaOpen(false)
+ setR1QaOpen(true)
+ setR2QaOpen(false)
+ }, [])
- showTranslateDiff() {
- if (this.state.translateDiffOn) {
- this.setState({
- translateDiffOn: false,
- })
+ const openR2Qa = useCallback(() => {
+ setAutomatedQaOpen(false)
+ setR1QaOpen(false)
+ setR2QaOpen(true)
+ }, [])
+
+ // Diff toggle handlers
+ const showTranslateDiff = useCallback(() => {
+ if (translateDiffOn) {
+ setTranslateDiffOn(false)
} else {
- let diffHtml = this.getDiffPatch(this.suggestion, this.target)
- this.setState({
- translateDiffOn: true,
- reviseDiffOn: false,
- revise2DiffOn: false,
- htmlDiff: diffHtml,
- })
+ setTranslateDiffOn(true)
+ setReviseDiffOn(false)
+ setRevise2DiffOn(false)
+ setHtmlDiff(TextUtils.getDiffHtml(suggestion, target))
}
- }
- showReviseDiff() {
- if (this.state.reviseDiffOn) {
- this.setState({
- reviseDiffOn: false,
- })
+ }, [translateDiffOn, suggestion, target])
+
+ const showReviseDiff = useCallback(() => {
+ if (reviseDiffOn) {
+ setReviseDiffOn(false)
} else {
- let revise = this.revise
- let textToDiff = this.target ? this.target : this.suggestion
- let diffHtml = this.getDiffPatch(textToDiff, revise)
- this.setState({
- translateDiffOn: false,
- reviseDiffOn: true,
- revise2DiffOn: false,
- htmlDiff: diffHtml,
- })
+ setTranslateDiffOn(false)
+ setReviseDiffOn(true)
+ setRevise2DiffOn(false)
+ setHtmlDiff(TextUtils.getDiffHtml(target || suggestion, revise))
}
- }
- showRevise2Diff() {
- if (this.state.revise2DiffOn) {
- this.setState({
- revise2DiffOn: false,
- })
+ }, [reviseDiffOn, target, suggestion, revise])
+
+ const showRevise2Diff = useCallback(() => {
+ if (revise2DiffOn) {
+ setRevise2DiffOn(false)
} else {
- let revise2 = this.revise2
- let textToDiff = this.revise
- ? this.revise
- : this.target
- ? this.target
- : this.suggestion
- let diffHtml = this.getDiffPatch(textToDiff, revise2)
- this.setState({
- translateDiffOn: false,
- reviseDiffOn: false,
- revise2DiffOn: true,
- htmlDiff: diffHtml,
- })
- }
- }
- getWordsSpeed() {
- let str_pad_left = function (string, pad, length) {
- return (new Array(length + 1).join(pad) + string).slice(-length)
- }
- let time = parseInt(this.props.segment.get('secs_per_word'))
- let minutes = Math.floor(time / 60)
- let seconds = time - minutes * 60
- if (minutes > 0) {
- return (
- str_pad_left(minutes, '0', 2) +
- "'" +
- str_pad_left(seconds, '0', 2) +
- "''"
+ setTranslateDiffOn(false)
+ setReviseDiffOn(false)
+ setRevise2DiffOn(true)
+ setHtmlDiff(
+ TextUtils.getDiffHtml(revise || target || suggestion, revise2),
)
- } else {
- return str_pad_left(seconds, '0', 2) + "''"
}
- }
- // getTimeToEdit() {
- // let str_pad_left = function(string,pad,length) {
- // return (new Array(length+1).join(pad)+string).slice(-length);
- // };
- // let time = parseInt(this.props.segment.get("time_to_edit")/1000);
- // let hours = Math.floor(time / 3600);
- // let minutes = Math.floor( time / 60);
- // let seconds = parseInt(time - minutes * 60);
- // if (hours > 0 ) {
- // return str_pad_left(hours,'0',2)+''+str_pad_left(minutes,'0',2)+"'"+str_pad_left(seconds,'0',2)+"''";
- // } else if (minutes > 0) {
- // return str_pad_left(minutes,'0',2)+"'"+str_pad_left(seconds,'0',2)+"''";
- // } else {
- // return str_pad_left(seconds,'0',2)+"''";
- // }
- //
- // }
- getDiffPatch(source, text) {
- return TextUtils.getDiffHtml(source, text)
- }
- openTranslateLink() {
- window.open(
- this.props.urls.get('translate_url') + '#' + this.props.segment.get('id'),
- )
- }
+ }, [revise2DiffOn, revise, target, suggestion, revise2])
- openReviseLink(revise) {
- if (
- typeof this.props.urls.get('revise_url') === 'string' ||
- this.props.urls.get('revise_url') instanceof String
- ) {
- window.open(
- this.props.urls.get('revise_url') + '#' + this.props.segment.get('id'),
- )
- } else {
- let url = this.props.urls
- .get('revise_urls')
- .find((value) => {
- return value.get('revision_number') === revise
- })
- .get('url')
- window.open(url + '#' + this.props.segment.get('id'))
- }
- }
+ const getAutomatedQaHtml = useCallback(() => {
+ const details = segment.get('warnings').get('details').get('issues_info')
- decodeTextAndTransformTags(text, isRtl) {
- if (text) {
- // Fix for more than 2 followed spaces
- text = text.replace(/ /gi, ' ')
- let decodedText = DraftMatecatUtils.transformTagsToHtml(text, isRtl)
- return decodedText
- }
- return text
- }
- allowHTML(string) {
- return {__html: string}
- }
- componentDidUpdate(prevProps) {
- if (prevProps.revisionToShow !== this.props.revisionToShow) {
- this.setState({
- r1QaOpen: this.props.revisionToShow === '1',
- r2QaOpen: this.props.revisionToShow === '2',
- })
- }
- }
+ return QA_TYPES.flatMap((type) => {
+ const categories = details.get(type).get('Categories')
+ if (categories.size === 0) return []
+ return categories
+ .entrySeq()
+ .map(([key, value]) => (
+
+
+ {QA_ICONS[type]}
+
+
+ {QA_CATEGORY_LABELS[key]} ({value.size})
+
+
+ ))
+ .toArray()
+ })
+ }, [segment])
- render() {
- let source = this.decodeTextAndTransformTags(
- this.source,
- config.isSourceRTL,
- )
- let suggestion = this.decodeTextAndTransformTags(
- this.suggestion,
- config.isTargetRTL,
- )
- let target =
- this.target &&
- this.decodeTextAndTransformTags(this.target, config.isTargetRTL)
- let revise =
- this.revise &&
- this.decodeTextAndTransformTags(this.revise, config.isTargetRTL)
- let revise2 =
- this.revise2 &&
- this.decodeTextAndTransformTags(this.revise2, config.isTargetRTL)
-
- if (this.state.translateDiffOn) {
- target = this.decodeTextAndTransformTags(
- this.state.htmlDiff,
- config.isTargetRTL,
- )
- }
+ const renderHumanQaIssues = useCallback(
+ (issues) =>
+ issues
+ .map((issue, index) => (
+
+ ))
+ .toArray(),
+ [],
+ )
- if (this.state.reviseDiffOn) {
- revise = this.decodeTextAndTransformTags(
- this.state.htmlDiff,
- config.isTargetRTL,
- )
+ const getWordsSpeed = useCallback(() => {
+ const time = parseInt(segment.get('secs_per_word'))
+ const minutes = Math.floor(time / 60)
+ const seconds = time - minutes * 60
+ if (minutes > 0) {
+ return `${strPadLeft(minutes, '0', 2)}'${strPadLeft(seconds, '0', 2)}''`
}
+ return `${strPadLeft(seconds, '0', 2)}''`
+ }, [segment])
- if (this.state.revise2DiffOn) {
- revise2 = this.decodeTextAndTransformTags(
- this.state.htmlDiff,
- config.isTargetRTL,
- )
- }
+ const openTranslateLink = useCallback(() => {
+ window.open(`${urls.get('translate_url')}#${segment.get('id')}`)
+ }, [urls, segment])
- let sourceClass = classnames({
- 'segment-container': true,
- 'qr-source': true,
- 'rtl-lang': config.isSourceRTL,
- })
+ const openReviseLink = useCallback(
+ (reviseNum) => {
+ const reviseUrl = urls.get('revise_url')
+ if (typeof reviseUrl === 'string' || reviseUrl instanceof String) {
+ window.open(`${reviseUrl}#${segment.get('id')}`)
+ } else {
+ const url = urls
+ .get('revise_urls')
+ .find((value) => value.get('revision_number') === reviseNum)
+ .get('url')
+ window.open(`${url}#${segment.get('id')}`)
+ }
+ },
+ [urls, segment],
+ )
- let segmentBodyClass = classnames({
- 'qr-segment-body': true,
- 'qr-diff-on':
- this.state.translateDiffOn ||
- this.state.reviseDiffOn ||
- this.state.revise2DiffOn,
- })
- let suggestionClasses = classnames({
- 'segment-container': true,
- 'qr-suggestion': true,
- 'shadow-1':
- this.state.translateDiffOn ||
- (this.state.reviseDiffOn && !this.target) ||
- (this.state.revise2DiffOn && !this.revise && !this.target),
- 'rtl-lang': config.isTargetRTL,
- })
- let translateClasses = classnames({
- 'segment-container': true,
- 'qr-translated': true,
- 'shadow-1':
- this.state.translateDiffOn ||
- this.state.reviseDiffOn ||
- (this.state.revise2DiffOn && !this.revise),
- 'rtl-lang': config.isTargetRTL,
- })
- let revisedClasses = classnames({
- 'segment-container': true,
- 'qr-revised': true,
- 'shadow-1': this.state.reviseDiffOn || this.state.revise2DiffOn,
- 'rtl-lang': config.isTargetRTL,
+ const renderSegmentHistory = () => {
+ const history = segment
+ .get('history')
+ .toJS()
+ .filter((elem) => elem.status)
+ return history.map((elem, index) => {
+ return (
+
+
+
+ {index < history.length - 1 && (
+
+ )}
+ {elem.status === SEGMENTS_STATUS.APPROVED2
+ ? '2nd Revision'
+ : elem.status.charAt(0).toUpperCase() +
+ elem.status.toLowerCase().slice(1)}
+
+
+ {elem.date.replaceAll('-', '/')}
+
+
+ {elem.issues.length > 0 && (
+
+ {elem.issues.map((issue) => (
+
+ {issue.issue_category}:{' '}
+
+ {issue.issue_severity}
+
+
+ ))}
+
+ }
+ >
+
+
+
+ Issues
+
+
+
+ )}
+
+ )
})
- let revised2Classes = classnames({
- 'segment-container': true,
- 'qr-revised': true,
- 'qr-revised-2ndpass': true,
- 'shadow-1': this.state.revise2DiffOn,
+ }
+
+ // Render logic
+ const renderedSource = decodeTextAndTransformTags(source, config.isSourceRTL)
+ const renderedSuggestion = decodeTextAndTransformTags(
+ suggestion,
+ config.isTargetRTL,
+ )
+ const renderedTarget = translateDiffOn
+ ? decodeTextAndTransformTags(htmlDiff, config.isTargetRTL)
+ : target && decodeTextAndTransformTags(target, config.isTargetRTL)
+ const renderedRevise = reviseDiffOn
+ ? decodeTextAndTransformTags(htmlDiff, config.isTargetRTL)
+ : revise && decodeTextAndTransformTags(revise, config.isTargetRTL)
+ const renderedRevise2 = revise2DiffOn
+ ? decodeTextAndTransformTags(htmlDiff, config.isTargetRTL)
+ : revise2 && decodeTextAndTransformTags(revise2, config.isTargetRTL)
+
+ const isDiffOn = translateDiffOn || reviseDiffOn || revise2DiffOn
+ const sourceClass = classnames('segment-container', 'qr-source', {
+ 'rtl-lang': config.isSourceRTL,
+ })
+ const segmentBodyClass = classnames('qr-segment-body', {
+ 'qr-diff-on': isDiffOn,
+ })
+ const suggestionClasses = classnames('segment-container', 'qr-suggestion', {
+ 'shadow-1':
+ translateDiffOn ||
+ (reviseDiffOn && !target) ||
+ (revise2DiffOn && !revise && !target),
+ 'rtl-lang': config.isTargetRTL,
+ })
+ const translateClasses = classnames('segment-container', 'qr-translated', {
+ 'shadow-1': translateDiffOn || reviseDiffOn || (revise2DiffOn && !revise),
+ 'rtl-lang': config.isTargetRTL,
+ })
+ const revisedClasses = classnames('segment-container', 'qr-revised', {
+ 'shadow-1': reviseDiffOn || revise2DiffOn,
+ 'rtl-lang': config.isTargetRTL,
+ })
+ const revised2Classes = classnames(
+ 'segment-container',
+ 'qr-revised',
+ 'qr-revised-2ndpass',
+ {
+ 'shadow-1': revise2DiffOn,
'rtl-lang': config.isTargetRTL,
- })
- return (
-
}
diff --git a/public/js/components/settingsPanel/Contents/MachineTranslationTab/MTGlossary/MTGlossaryRow.js b/public/js/components/settingsPanel/Contents/MachineTranslationTab/MTGlossary/MTGlossaryRow.js
index 73f3d9c167..976583c8d8 100644
--- a/public/js/components/settingsPanel/Contents/MachineTranslationTab/MTGlossary/MTGlossaryRow.js
+++ b/public/js/components/settingsPanel/Contents/MachineTranslationTab/MTGlossary/MTGlossaryRow.js
@@ -18,6 +18,11 @@ import Close from '../../../../../../img/icons/Close'
import LabelWithTooltip from '../../../../common/LabelWithTooltip'
import CatToolActions from '../../../../../actions/CatToolActions'
import {SettingsPanelContext} from '../../../SettingsPanelContext'
+import {
+ Button,
+ BUTTON_SIZE,
+ BUTTON_TYPE,
+} from '../../../../common/Button/Button'
export const MTGlossaryRow = ({
engineId,
@@ -138,28 +143,36 @@ export const MTGlossaryRow = ({
}
const editingNameButtons = !isEditingName ? (
-
)
@@ -217,14 +230,16 @@ export const MTGlossaryRow = ({