diff --git a/i18n/core/en.json b/i18n/core/en.json index 4f163330..67fb7b79 100644 --- a/i18n/core/en.json +++ b/i18n/core/en.json @@ -50,6 +50,7 @@ "deputy.session.row.history": "Open page history", "deputy.session.row.checkAll": "Mark all revisions as finished", "deputy.session.row.checkAll.confirm": "Mark all revisions as finished?", + "deputy.session.row.additionalComments": "Discussion", "deputy.session.row.closeComments": "Closing comments", "deputy.session.row.close.sigFound": "The closing comment had a signature. It will not be automatically removed when saved.", "deputy.session.row.close.sigFound.maybe": "The closing comment might have had a signature. It will not be automatically removed when saved.", diff --git a/src/css/deputy.css b/src/css/deputy.css index c6628bfc..abb14b74 100644 --- a/src/css/deputy.css +++ b/src/css/deputy.css @@ -206,6 +206,23 @@ p.dp-messageWidget-message { vertical-align: middle; } +.dp-cs-row-comments { + padding: 16px; + background-color: rgba(0, 159, 255, 10%); + margin: 4px 0; +} + +.dp-cs-row-comments > b { + letter-spacing: 0.1em; + font-weight: bold; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.5); +} + +.dp-cs-row-comments hr { + border-color: rgb(0, 31, 51); +} + body.mediawiki.ltr .dp-cs-row-head > :not(:first-child):not(:last-child), body.mediawiki.ltr .dp-cs-row-head > :not(:first-child):not(:last-child) { margin-right: 16px; @@ -255,7 +272,7 @@ body.mediawiki.rtl .dp-cs-row-head > :not(:first-child):not(:last-child) { .dp-cs-row-content { padding: 16px; - background-color: rgba(0, 0, 0, 4%); + background-color: rgba(0, 0, 0, 6%); margin: 4px 0; } diff --git a/src/ui/root/DeputyContributionSurveyRow.tsx b/src/ui/root/DeputyContributionSurveyRow.tsx index bd575818..7ad74a33 100644 --- a/src/ui/root/DeputyContributionSurveyRow.tsx +++ b/src/ui/root/DeputyContributionSurveyRow.tsx @@ -71,6 +71,10 @@ export default class DeputyContributionSurveyRow extends EventTarget implements * The "LI" element that this row was rendered into by MediaWiki. */ originalElement?: HTMLLIElement; + /** + * Additional comments that may have been left by other editors. + */ + additionalComments: Element[]; /** * Original wikitext of this element. */ @@ -357,10 +361,85 @@ export default class DeputyContributionSurveyRow extends EventTarget implements super(); this.row = row; this.originalElement = originalElement; + this.additionalComments = this.extractAdditionalComments(); this.originalWikitext = originalWikitext; this.section = section; } + /** + * Extracts HTML elements which may be additional comments left by others. + * The general qualification for this is that it has to be a list block + * element that comes after the main line (in this case, it's detected after + * the last . + * This appears in the following form in wikitext: + * + * ``` + * * [[Page]] (...) [[Special:Diff/...|...]] + * *: Hello! <-- definition list block + * ** What!? <-- sub ul + * *# Yes. <-- sub ol + * * [[Page]] (...) [[Special:Diff/...|...]]
...
<-- inline div + * ``` + * + * Everything else (`*
...`, `*'''...`, `*`, etc.) is considered + * not to be an additional comment. + * + * If no elements were found, this returns an empty array. + * + * @return An array of HTMLElements + */ + extractAdditionalComments(): Element[] { + // COMPAT: Specific to MER-C contribution surveyor + // Initialize to first successive diff link. + let lastSuccessiveDiffLink = this.originalElement.querySelector( + 'a[href^="/wiki/Special:Diff/"]' + ); + + const elements: Element[] = []; + if ( !lastSuccessiveDiffLink ) { + // No diff links. Get last element, check if block element, and crawl backwards. + let nextDiscussionElement = this.originalElement.lastElementChild; + while ( + nextDiscussionElement && + window.getComputedStyle( nextDiscussionElement, '' ).display === 'block' + ) { + elements.push( nextDiscussionElement ); + + nextDiscussionElement = nextDiscussionElement.previousElementSibling; + } + } else { + while ( + lastSuccessiveDiffLink.nextElementSibling && + lastSuccessiveDiffLink.nextElementSibling.tagName === 'A' && + lastSuccessiveDiffLink + .nextElementSibling + .getAttribute( 'href' ) + .startsWith( '/wiki/Special:Diff' ) + ) { + lastSuccessiveDiffLink = lastSuccessiveDiffLink.nextElementSibling; + } + // The first block element after `lastSuccessiveDiffLink` is likely discussion, + // and everything after it is likely part of such discussion. + let pushing = false; + let nextDiscussionElement = lastSuccessiveDiffLink.nextElementSibling; + while ( nextDiscussionElement != null ) { + if ( + !pushing && + window.getComputedStyle( nextDiscussionElement ).display === 'block' + ) { + pushing = true; + elements.push( nextDiscussionElement ); + } else if ( pushing ) { + elements.push( nextDiscussionElement ); + } + + nextDiscussionElement = nextDiscussionElement.nextElementSibling; + } + } + + return elements; + } + /** * Load the revision data in and change the UI element respectively. */ @@ -475,7 +554,7 @@ export default class DeputyContributionSurveyRow extends EventTarget implements this.commentsTextInput = new OO.ui.MultilineTextInputWidget( { classes: [ 'dp-cs-row-closeComments' ], placeholder: mw.msg( 'deputy.session.row.closeComments' ), - value: value, + value: value ?? '', autosize: true, rows: 1 } ); @@ -535,7 +614,7 @@ export default class DeputyContributionSurveyRow extends EventTarget implements revisionList.appendChild( unwrapWidget( this.unfinishedMessageBox ) ); revisionList.appendChild( unwrapWidget( - this.renderCommentsTextInput() + this.renderCommentsTextInput( this.row.comment ) ) ); for ( const revision of diffs.values() ) { @@ -791,6 +870,27 @@ export default class DeputyContributionSurveyRow extends EventTarget implements
; } + /** + * Renders additional comments that became part of this row. + * + * @return An HTML element. + */ + renderAdditionalComments(): JSX.Element { + const additionalComments =
+ { mw.msg( 'deputy.session.row.additionalComments' ) } +
+
e.innerHTML ).join( '' ) + } /> +
; + + // Open all links in new tabs. + additionalComments.querySelectorAll( '.dp-cs-row-comments-content a' ) + .forEach( a => a.setAttribute( 'target', '_blank' ) ); + + return additionalComments; + } + /** * * @param diffs @@ -810,6 +910,7 @@ export default class DeputyContributionSurveyRow extends EventTarget implements this.element = swapElements( this.element,
{ this.renderHead( diffs, contentContainer ) } + { this.additionalComments?.length > 0 && this.renderAdditionalComments() } { contentContainer }
) as HTMLElement; diff --git a/src/ui/root/DeputyContributionSurveySection.tsx b/src/ui/root/DeputyContributionSurveySection.tsx index 477fd7f6..47b4aefe 100644 --- a/src/ui/root/DeputyContributionSurveySection.tsx +++ b/src/ui/root/DeputyContributionSurveySection.tsx @@ -294,7 +294,7 @@ export default class DeputyContributionSurveySection implements DeputyUIElement return false; } - this.originalList = firstList.parentElement.removeChild( firstList ) as HTMLElement; + this.originalList = firstList as HTMLElement; const rowElements: Record = {}; for ( let i = 0; i < this.originalList.children.length; i++ ) { @@ -343,6 +343,9 @@ export default class DeputyContributionSurveySection implements DeputyUIElement this.wikitextLines.push( rowElement ); } + // Remove last, this is to preserve as much state as possible + firstList.parentElement.removeChild( firstList ); + return true; } diff --git a/src/util/pickSequence.ts b/src/util/pickSequence.ts new file mode 100644 index 00000000..4fa5ae1e --- /dev/null +++ b/src/util/pickSequence.ts @@ -0,0 +1,33 @@ +/** + * Iterates over an array and returns an Iterator which checks each element + * of the array sequentially for a given condition (predicated by `condition`) + * and returns another array, containing an element where `true` was returned, + * and every subsequent element where the check returns `false`. + * + * @param arr + * @param condition + * @yield The found sequence + */ +export default function* pickSequence( + arr: T[], + condition: ( val: T ) => boolean +): Iterable { + let currentValues: T[] = null; + let shouldReturnValues = false; + for ( const val of arr ) { + if ( condition( val ) ) { + shouldReturnValues = true; + if ( currentValues != null ) { + yield currentValues; + } + currentValues = [ val ]; + continue; + } + if ( shouldReturnValues ) { + currentValues.push( val ); + } + } + if ( currentValues.length > 0 ) { + yield currentValues; + } +} diff --git a/tests/unit/ContributionSurveyRowParserTests.ts b/tests/unit/ContributionSurveyRowParserTests.ts index 2a2e546b..f49d43eb 100644 --- a/tests/unit/ContributionSurveyRowParserTests.ts +++ b/tests/unit/ContributionSurveyRowParserTests.ts @@ -350,7 +350,7 @@ describe( 'ContributionSurveyRowParser line parsing tests', () => { } ); } ); - test( 'edge: comment after diffs', () => { + test( 'edge: comment after diffs (unfinished, has comment)', () => { const parser = new ContributionSurveyRowParser( '* [[:Example]]: (1 edit, 1 major, +173) [[Special:Diff/123456|(+173)]] muy bien' ); diff --git a/tests/unit/browser/UtilityUnitTests.ts b/tests/unit/browser/UtilityUnitTests.ts index a1cd006c..93812ae9 100644 --- a/tests/unit/browser/UtilityUnitTests.ts +++ b/tests/unit/browser/UtilityUnitTests.ts @@ -2,7 +2,7 @@ import '../../../src/types'; import 'types-mediawiki'; import BrowserHelper from '../../util/BrowserHelper'; -describe( 'Utility function tests', () => { +describe( 'Utility (on-browser) function tests', () => { let page: BrowserHelper; diff --git a/tests/unit/util/UtilityUnitTests.ts b/tests/unit/util/UtilityUnitTests.ts new file mode 100644 index 00000000..95d8c31b --- /dev/null +++ b/tests/unit/util/UtilityUnitTests.ts @@ -0,0 +1,56 @@ +import pickSequence from '../../../src/util/pickSequence'; + +describe( 'Utility function tests', () => { + + test( 'pickSequence', async () => { + expect( Array.from( + pickSequence( + [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], + ( v ) => v % 3 === 0 + ) + ) ).toStrictEqual( + [ + [ 3, 4, 5 ], + [ 6, 7, 8 ], + [ 9, 10 ] + ] + ); + } ); + + test( 'pickSequence', async () => { + expect( Array.from( + pickSequence( + [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], + ( v ) => v % 2 === 0 + ) + ) ).toStrictEqual( + [ + [ 2, 3 ], [ 4, 5 ], [ 6, 7 ], + [ 8, 9 ], [ 10 ] + ] + ); + } ); + + test( 'pickSequence', async () => { + expect( Array.from( + pickSequence( + [ + '* line 1', + '* line 2', + '*: comment for line 2', + '* line 3', + '* line 4' + ], + ( v ) => /\*[^:*#]/.test( v ) + ) + ) ).toStrictEqual( + [ + [ '* line 1' ], + [ '* line 2', '*: comment for line 2' ], + [ '* line 3' ], + [ '* line 4' ] + ] + ); + } ); + +} );