User:DannyS712/VueNPP.js

// // Script to experiment with a Vue version of Special:NewPagesFeed // @author DannyS712 /* jshint maxerr: 999, esversion: 9, esnext: false */ $( => { const VueNPP = {}; window.VueNPP = VueNPP;

VueNPP.init = function { mw.loader.using(		[			'vue',			'@vue/composition-api',			'wvui',			'mediawiki.util',			'mediawiki.api',			'moment',			// So that messages and styles are loaded			'ext.pageTriage.views.list'		],		VueNPP.run	); };

VueNPP.run = function { const VueCompositionAPI = mw.loader.require( '@vue/composition-api' ); // Exposed globally for simplicity window.VueCompositionAPI = VueCompositionAPI; Vue.use( VueCompositionAPI );

const wvuiComponents = mw.loader.require( 'wvui' ); VueNPP.listItemComponent.components = wvuiComponents; VueNPP.loadMoreBarComponent.components = wvuiComponents; // Object.assign for the components that have other non-wvui components Object.assign( VueNPP.feedControlMenuComponent.components, wvuiComponents ); Object.assign( VueNPP.feedContentsComponent.components, wvuiComponents ); VueNPP.addStyle; VueNPP.renderInterface; };

/** * Add styles for our interface. */ VueNPP.addStyle = function { mw.util.addCSS(` .mwe-vue-pt-metadata-warning:before {	color: initial;	content: " · "; } .mwe-vue-pt-button-green {	/* From core and vector styles for green ui buttons */	border-color: #294 !important;	background: #295 !important;	background: linear-gradient( to bottom, #3c8 0%, #295 90%) !important;	border-radius: 4px;	box-shadow: 0 1px 3px; } .mwe-vue-pt-button-green:disabled {	opacity: .35; } .mwe-vue-pt-navigation-bar {	border: 1px solid #ccc; } .mwe-vue-pt-control-gradient {	background: #c9c9c9; }	padding: 0.5em 1em 1em 1em;	position: sticky;	top: 0;	z-index: 10;	box-shadow: 0 7px 10px rgba( 0, 0, 0, 0.4 ); } .mwe-vue-pt-control-section {	min-width: 200px;	margin: 0.4em 0.4em 0 0.4em;	z-index: 51; } .mwe-vue-pt-control-label-right,	float: right; } .mwe-vue-pt-control-options {	margin-left: 1em;	margin-right: 0.5em;	white-space: nowrap; } .mwe-vue-pt-control-buttons {	margin: 0.2em 0 0 -0.4em; }	position: absolute; z-index: 50; border: 1px solid #aaa; padding: 0.5em 1em 0.2em 1em; margin-left: 48px; color: #000; cursor: default; box-shadow: 0 7px 10px rgba( 0, 0, 0, 0.4 ); width: min-content; } .mwe-vue-pt-control-section__row1 { display: flex; flex-direction: row; }	width: 100px; }	margin-right: 0.3em; }	margin-left: 10px; }	color: #0645ad; cursor: pointer; }	text-align: center; font-size: 17px; background-color: #e8f2f8; margin: 0; padding: 0.4em; border: 1px solid #ccc; border-top: 0; }   margin: auto; /* Override WVUI styles to make a quieter version */ background-color: inherit; border: none; }
 * 1) mwe-vue-pt-menu-heading {
 * 1) mwe-vue-pt-refresh-button {
 * 1) mwe-vue-pt-control-dropdown {
 * 1) mwe-vue-pt-filter-user {
 * 1) mwe-vue-pt-sort-buttons {
 * 1) mwe-vue-pt-radio-afc {
 * 1) mwe-vue-pt-control-menu-toggle {
 * 1) mwe-vue-pt-feed-load-more {
 * 1) mwe-vue-pt-feed-load-more .wvui-progress-bar {

.mwe-vue-pt-article-row-even { background-color: #f1f1f1; } .mwe-vue-pt-article-row-odd { background-color: #fff; } .mwe-vue-pt-info-pane { padding: 0.5em 0.6em 0.6em 2.7em; min-height: 4.8em; display: table; box-sizing: border-box; width: 100%; } .mwe-vue-pt-info-row { display: table-row; vertical-align: top; } .mwe-vue-pt-info-row > div { display: table-cell; } .mwe-vue-pt-status-icon { position: absolute; top: 5px; left: 5px; } .mwe-vue-pt-article-row { position: relative; border: 1px solid #ccc; border-top: 0; } /* info about the article */ .mwe-vue-pt-article { font-size: 1.1em; line-height: 1.6em; } .mwe-vue-pt-bold { font-weight: bold; } .mwe-vue-pt-metadata-warning, .mwe-vue-pt-issue { color: #c00; font-weight: bold; } /* Info on the right hand side: creation date, updated date, potential isues, etc. */ .mwe-vue-pt-article-col-right { text-align: right; white-space: nowrap; } /* the article snippet */ .mwe-vue-pt-snippet { color: #808080; padding-right: 1em; vertical-align: top; }

/* Navigation bar at the bottom */ min-height: 50px; border-top: 1px solid #ccc; position: sticky; bottom: 0; z-index: 1; box-shadow: 0 -7px 10px rgba( 0, 0, 0, 0.4 ); }	padding: 0.5em 1em; }	`); };
 * 1) mwe-vue-pt-stats-navigation {
 * 1) mwe-vue-pt-stats-navigation-content {

//const VueNPP = {};

//#region devCode /** * Component for rendering a list item. */ VueNPP.listItemComponent = { // wvuiButton is added later, once it has been loaded // defaults for props are from FDP Hamburg for now props: { position: { type: Number, default: 1 }, afdStatus: { type: Boolean, default: false }, blpProdStatus: { type: Boolean, default: false }, csdStatus: { type: Boolean, default: false }, prodStatus: { type: Boolean, default: false }, patrolStatus: { type: Number, default: 0 }, title: { type: String, default: 'FDP Hamburg' }, isRedirect: { type: Boolean, default: false }, categoryCount: { type: Number, default: 3 }, linkCount: { type: Number, default: 0 }, referenceCount: { type: Number, default: 1 }, recreated: { type: Boolean, default: false }, pageLen: { type: Number, default: 3760 }, revCount: { type: Number, default: 2 }, creationDateUTC: { type: String, default: '20220523131514' }, creatorName: { type: String, default: 'Wanquanbiantai' }, creatorAutoConfirmed: { type: Boolean, default: true }, creatorRegistrationUTC: { type: String, default: '20220317205608' }, creatorUserId: { type: Number, default: 43566998 }, creatorEditCount: { type: Number, default: 66 }, creatorIsBot: { type: Boolean, default: false }, creatorBlocked: { type: Boolean, default: false }, creatorUserPageExists: { type: Boolean, default: true }, creatorTalkPageExists: { type: Boolean, default: true }, afcState: { type: Number, default: 1 }, reviewedUpdatedUTC: { type: String, default: '20220523131514' }, snippet: { type: String, default: 'Chair Logo Michael Kruse FDP LV Hamburg Basis data Established: September 20, 1945 Place of establishment: Hamburg Chairman: Michael Kruse Vice chairmen: Katarina BlumeRia SchröderAndreas MoringSonja Jacobsen Treasurer: Ron Schumacher Executive direct...' }, oresArticleQuality: { type: String, default: 'Start' }, oresDraftQuality: { type: String, default: '' }, copyvio: { type: Number, default: 0 }, },   data: function  { return { showOres: true || mw.config.get( 'wgShowOresFilters' ), showCopyvio: true || mw.config.get( 'wgShowCopyvio' ), enableReviewButton: true || mw.config.get( 'wgPageTriageEnableReviewButton' ), draftNamespaceId: mw.config.get( 'wgPageTriageDraftNamespaceId' ), timeOffset: parseInt( mw.user.options.get( 'timecorrection' ).split( '|' )[ 1 ] ) };   },    methods: { prettyTimestamp: function ( utcTimestamp ) { const parsedTimestamp = moment.utc( utcTimestamp, 'YYYYMMDDHHmmss' ); return parsedTimestamp.utcOffset( this.timeOffset ).format(               mw.msg( 'pagetriage-creation-dateformat' )            ); },       getjQueryLink: function ( url, text, exists ) { // Needed to be able to embed links in the byline const $link = $( '' ); if ( !exists ) { const uri = new mw.Uri( url ); uri.query.action = 'edit'; uri.query.redlink = 1; url = uri.toString; $link.addClass( 'new' ); }           $link.attr( 'href', url ); $link.text( text ); return $link; }   },    computed: { oddEvenClass: function { return this.position % 2 == 0 ? 'mwe-vue-pt-article-row-even' : 'mwe-vue-pt-article-row-odd'; }, isDraft: function { const pageNamespaceId = ( new mw.Title( this.title ) ).getNamespaceId; return pageNamespaceId === this.draftNamespaceId; },       iconImageSrc: function  { const imageBase = mw.config.get( 'wgExtensionAssetsPath' ) + '/PageTriage/modules/ext.pageTriage.views.list/images/'; if ( this.isDraft ) { return imageBase + 'icon_not_reviewed.png'; } else if ( this.afdStatus || this.blpProdStatus || this.csdStatus || this.prodStatus ) { return imageBase + 'icon_marked_for_deletion.png'; } else if ( this.patrolStatus !== 0 ) { return imageBase + 'icon_reviewed.png'; } else { return imageBase + 'icon_not_reviewed.png'; }       },        titleUrl: function  { const params = {}; if ( this.isRedirect ) { params.redirect = 'no'; }           return mw.util.getUrl( this.title, params ); },       titleUrlFormat: function  { return mw.util.wikiUrlencode( this.title ); }, historyUrl: function { return mw.config.get('wgScriptPath') + '/index.php?title=' + this.titleUrlFormat + '&action=history'; }, creationDatePretty: function { return this.prettyTimestamp( this.creationDateUTC ); },       creatorBylineHtml: function  { const bylineMessage = ( this.creatorUserId > 0 && !this.creatorAutoConfirmed ) ? 'pagetriage-byline-new-editor' : 'pagetriage-byline'; const creatorUserPageUrl = mw.util.getUrl( 'User:' + this.creatorName ); const creatorTalkPageUrl = mw.util.getUrl( 'User talk:' + this.creatorName ); const contribsUrl = mw.util.getUrl( 'Special:Contributions/' + this.creatorName ); return mw.message(               bylineMessage,                this.getjQueryLink( creatorUserPageUrl, this.creatorName, this.creatorUserPageExists ),               this.getjQueryLink( creatorTalkPageUrl, mw.msg( 'sp-contributions-talk' ), this.creatorTalkPageExists ),               mw.msg( 'pipe-separator' ),                this.getjQueryLink( contribsUrl, mw.msg( 'contribslink' ), true )           ).parse; },       creatorRegistrationPretty: function  { return this.prettyTimestamp( this.creatorRegistrationUTC ); },       reviewedUpdatedPretty: function  { return this.prettyTimestamp( this.reviewedUpdatedUTC ); },       lastAfcActionLabel: function  { if ( this.afcState === 2 ) { return 'pagetriage-afc-date-label-submission'; } else if ( this.afcState === 3 ) { return 'pagetriage-afc-date-label-review'; } else if ( this.afcState === 4 ) { return 'pagetriage-afc-date-label-declined'; }           return ''; },       reviewRightHelpText: function  { if ( this.enableReviewButton ) { return ''; }           return this.$i18n( 'pagetriage-no-patrol-right' ); },       copyvioLink: function  { if ( this.copyvio === 0 ) { // Shouldn't be used return ''; }           return 'https://tools.wmflabs.org/copypatrol/en?filter=all&searchCriteria=page_exact' + '&searchText=' + ( new mw.Title( this.title ) ).getMainText + '&drafts=' + ( this.isDraft ? '1' : '0' ) + '&revision=' + this.copyvio; }   },    template: `   () ·					·					   · 					     0">					·						·								   Review												<span v-if="copyvio && showCopyvio">				 · 					<a :href="copyvioLink" target="_blank" class="external">					</a> ` };

/** * Helper for controls form, contains a specific section with a message label * and slot content */ VueNPP.controlSectionComponent = { props: { label: { type: String, required: true } }, template: `  ` };

/** * Helper for controls form, contains controls for date ranges */ VueNPP.dateControlSectionComponent = { props: { type: { type: String, required: true }, fromModel: { type: String, required: true }, toModel: { type: String, required: true } },   components: { controlSection: VueNPP.controlSectionComponent }, methods: { updateFrom: function ( newValue ) { this.$emit( 'update:fromModel', newValue.target.value ); },       updateTo: function ( newValue ) { this.$emit( 'update:toModel', newValue.target.value ); }   },    template: ` <control-section label="pagetriage-filter-date-range-heading"> <label :for="'mwe-vue-pt-filter-' + type + '-date-range-from'"> <input type="date" :value="fromModel" @input="updateFrom" :id="'mwe-vue-pt-filter-' + type + '-date-range-from'" :placeholder="$i18n( 'pagetriage-filter-date-range-format-placeholder' )" /> <label :for="'mwe-vue-pt-filter-' + type + '-date-range-to'"> <input type="date" :value="toModel" @input="updateTo" :id="'mwe-vue-pt-filter-' + type + '-date-range-to'" :placeholder="$i18n( 'pagetriage-filter-date-range-format-placeholder' )" /> </control-section>` };

VueNPP.lastGeneratedIdNum = 0; VueNPP.labeledInputComponent = { // For some reason things break if I try to just use v-model, though v-model:model-value // works (in the places where this component is used), but since it needs to be named // anyway its called input-model to make it clear that its used for the props: { // id is used to associated input with the via `for`, if not // provided auto generate one inputId: { type: String, default: => `mwe-vue-pt-generated-${++VueNPP.lastGeneratedIdNum}` },       inputModel: { type: [ String, Boolean ], required: true }, labelMsg: { type: String, required: true }, type: { type: String, required: true }, // only needed for radios, not checkboxes value: { type: String, default: '' }, noBreak: { type: Boolean, default: false } },   emits: [ 'update:inputModel' ], setup( props, { emit } ) { const isChecked = VueCompositionAPI.computed( => {            if ( props.type === 'radio' ) {                return ( props.inputModel === props.value );            } else if ( props.type === 'checkbox' ) {                return ( props.inputModel === true );            } else {                return false;            }        } ); const onChange = function ( event ) { const newValue = ( props.type === 'radio' ? event.target.value : event.target.checked ); emit( 'update:inputModel', newValue ); };       const haveBreak = VueCompositionAPI.computed(  => !props.noBreak ); return { isChecked, onChange, haveBreak }; },   template: ` <input :type="type" :id="inputId" :value="value" :checked="isChecked" @change="onChange" /> <label :for="inputId"> <br v-if="haveBreak" /> ` };

/** * Convert afc submission state name to api value * PageTriage extension uses literal 'all' with breaks things, use `false` so * that mw.Api filters it out, T304574 */ VueNPP.getAfcStateForApi = function ( stateName ) { const submissionNumbers = [ '~invalid~', 'unsubmitted', 'pending', 'reviewing', 'declined' ]; const stateIndex = submissionNumbers.indexOf( stateName ); return ( stateIndex <= 0 ? false : stateIndex.toString ); }; /** * Menu for controlling the filters for the pages feed */ VueNPP.feedControlMenuComponent = { components: { controlSection: VueNPP.controlSectionComponent, dateControlSection: VueNPP.dateControlSectionComponent, labeledInput: VueNPP.labeledInputComponent },   props: { currentlyShowingText: { type: String, default: 'currentlyShowingText-value' }, currentFilteredCount: { type: Number, default: -1 }, // Some form elements, sorting direction and which view we are in, trigger // updates to the feed immediately, others need to be submitted. Regardless, // we initialize the references with the current prop value, and then either // when the property changes or the menu is submitted, we emit the overall // updated object startOptions: { type: Object, default: => ( {                currentView: 'npp',                nppSortDir: 'newestfirst',                nppNamespace: 0,                nppIncludeUnreviewed: true,                nppIncludeReviewed: true,                nppIncludeNominated: true,                nppIncludeRedirects: false,                nppIncludeOthers: true,				nppFilter: 'all',				nppFilterUser: ,				nppPredictedRating: {                    stub: false,                    start: false,                    c: false,                    b: false,                    good: false,                    featured: false				},				nppPossibleIssues: {				    vandalism: false,                    spam: false,                    attack: false,                    copyvio: false,                    none: false				},				nppDateFrom: ,				nppDateTo: '',				afcSortDir: 'newestfirst',				afcSubmissionState: 'all',				afcPredictedRating: { stub: false, start: false, c: false, b: false, good: false, featured: false },				afcPossibleIssues: { vandalism: false, spam: false, attack: false, copyvio: false, none: false },				afcDateFrom: '', afcDateTo: '', } )       }    },    data: function  {        return {            haveDraftNamespace: true || !!mw.config.get( 'wgPageTriageDraftNamespaceId' ),            showOresFilters: true || mw.config.get( 'wgShowOresFilters' ),            showCopyvio: true || mw.config.get( 'wgShowCopyvio' ),            // pure data, not needed in setup			nppFilters: [                'no-categories',				'unreferenced',				'orphan',				'recreated',				'non-autoconfirmed',				'learners',				'blocked',				'bot-edits'				// user specific filter, and then show all, handled individually			],            afcSubmissionStates: [                'unsubmitted',				'pending',				'reviewing',				'declined',				'all'			]        };    },    setup( props, { emit } ) {        // Shortcuts        const ref = VueCompositionAPI.ref;        const computed = VueCompositionAPI.computed;        const watch = VueCompositionAPI.watch;

//#region startValues const currentView = ref( props.startOptions.currentView ); const nppSortDir = ref( props.startOptions.nppSortDir ); const nppNamespace = ref( props.startOptions.nppNamespace ); const nppIncludeUnreviewed = ref( props.startOptions.nppIncludeUnreviewed ); const nppIncludeReviewed = ref( props.startOptions.nppIncludeReviewed ); const nppIncludeNominated = ref( props.startOptions.nppIncludeNominated ); const nppIncludeRedirects = ref( props.startOptions.nppIncludeRedirects ); const nppIncludeOthers = ref( props.startOptions.nppIncludeOthers ); const nppFilter = ref( props.startOptions.nppFilter ); const nppFilterUser = ref( props.startOptions.nppFilterUser ); const nppPredictedRating = ref( { ...props.startOptions.nppPredictedRating } ); const nppPossibleIssues = ref( { ...props.startOptions.nppPossibleIssues } ); const nppDateFrom = ref( props.startOptions.nppDateFrom ); const nppDateTo = ref( props.startOptions.nppDateTo ); const afcSortDir = ref( props.startOptions.afcSortDir ); const afcSubmissionState = ref( props.startOptions.afcSubmissionState ); const afcPredictedRating = ref( { ...props.startOptions.afcPredictedRating } ); const afcPossibleIssues = ref( { ...props.startOptions.afcPossibleIssues } ); const afcDateFrom = ref( props.startOptions.afcDateFrom ); const afcDateTo = ref( props.startOptions.afcDateTo ); //#endregion // if the submitted/declined sort options should be included, the end of       // the message key to use (pagetriage-afc-(old|new)est-*), or false to        // not include as options const afcSortUpdated = computed( => {            if ( afcSubmissionState.value === 'declined' ) {                return 'declined';            } else if ( afcSubmissionState.value === 'pending' || afcSubmissionState.value === 'reviewing' ) {               return 'submitted';            }            return false;        } ); // Make sure that afcSortDir isn't invalid watch(           afcSubmissionState,            ( newState ) => {                if ( newState !== 'unsubmitted' && newState !== 'all' ) {                    // oldest/newest submitted/declined are valid                    return;                }                if ( afcSortDir.value === 'newestreview' ) {                    afcSortDir.value = 'newestfirst';                } else if ( afcSortDir.value === 'oldestreview' ) {                    afcSortDir.value = 'oldestfirst';                }            }        ); // Need to include at least one of reviewed/unreviewed, and at least // one of nominated for deletion/redirects/normal articles const canSaveSettings = computed( => {            return ( ( nppIncludeUnreviewed.value || nppIncludeReviewed.value ) && (                   nppIncludeNominated.value                    || nppIncludeRedirects.value                    || nppIncludeOthers.value                ) );       } );        // Whether the control menu is even shown at all const controlMenuOpen = ref( false ); const doSaveSettings = function { // need to convert the objects to raw (ores filters) const toRaw = VueCompositionAPI.toRaw; const settings = { currentView: currentView.value, nppSortDir: nppSortDir.value, nppNamespace: nppNamespace.value, nppIncludeUnreviewed: nppIncludeUnreviewed.value, nppIncludeReviewed: nppIncludeReviewed.value, nppIncludeNominated: nppIncludeNominated.value, nppIncludeRedirects: nppIncludeRedirects.value, nppIncludeOthers: nppIncludeOthers.value, nppFilter: nppFilter.value, nppFilterUser: nppFilterUser.value, nppPredictedRating: toRaw( nppPredictedRating.value ), nppPossibleIssues: toRaw( nppPossibleIssues.value ), nppDateFrom: nppDateFrom.value, nppDateTo: nppDateTo.value, afcSortDir: afcSortDir.value, afcSubmissionState: afcSubmissionState.value, afcPredictedRating: toRaw( afcPredictedRating.value ), afcPossibleIssues: toRaw( afcPossibleIssues.value ), afcDateFrom: afcDateFrom.value, afcDateTo: afcDateTo.value, };           emit( 'update-settings', settings ); // manually hide, next time that its opened the start options will // be updated controlMenuOpen.value = false; };

const toggleControlMenuIndicator = computed(            => ( controlMenuOpen.value ? '▾' : '▸' )       );        // On open, restore the start options to account for any prior changes, // on close, restore them because the current changes are being discarded const toggleControlMenu = => { // note that when closing due to an immediatelly handled change // (view or sort direction) this method is not called, but rather // the open status is changed manually, which is why start options // are also restored on open reapplyStartOptions; controlMenuOpen.value = !controlMenuOpen.value; };       // don't trigger watchers when these are reapplied const currentlyInReset = ref( false ); const reapplyStartOptions = => { currentlyInReset.value = true; currentView.value = props.startOptions.currentView; nppSortDir.value = props.startOptions.nppSortDir; nppNamespace.value = props.startOptions.nppNamespace; nppIncludeUnreviewed.value = props.startOptions.nppIncludeUnreviewed; nppIncludeReviewed.value = props.startOptions.nppIncludeReviewed; nppIncludeNominated.value = props.startOptions.nppIncludeNominated; nppIncludeRedirects.value = props.startOptions.nppIncludeRedirects; nppIncludeOthers.value = props.startOptions.nppIncludeOthers; nppFilter.value = props.startOptions.nppFilter; nppFilterUser.value = props.startOptions.nppFilterUser; nppPredictedRating.value = { ...props.startOptions.nppPredictedRating }; nppPossibleIssues.value = { ...props.startOptions.nppPossibleIssues }; nppDateFrom.value = props.startOptions.nppDateFrom; nppDateTo.value = props.startOptions.nppDateTo; afcSortDir.value = props.startOptions.afcSortDir; afcSubmissionState.value = props.startOptions.afcSubmissionState; afcPredictedRating.value = { ...props.startOptions.afcPredictedRating }; afcPossibleIssues.value = { ...props.startOptions.afcPossibleIssues }; afcDateFrom.value = props.startOptions.afcDateFrom; afcDateTo.value = props.startOptions.afcDateTo; currentlyInReset.value = false; };       // When the sort dir or the view changes, we want to immediately // update the settings to use that, ignoring any other changes made. // Close the menu so that when it is reopened, the start options are // reused, cancelling out the changes in the local state const handleImmediateChange = function ( changeName, changeVal ) { if ( currentlyInReset.value ) { // ignore return; }           // Make a *deep copy* of the start options const updatedSettings = { ...props.startOptions }; updatedSettings.nppPredictedRating = { ...props.startOptions.nppPredictedRating }; updatedSettings.nppPossibleIssues = { ...props.startOptions.nppPossibleIssues }; updatedSettings.afcPredictedRating = { ...props.startOptions.afcPredictedRating }; updatedSettings.afcPossibleIssues = { ...props.startOptions.afcPossibleIssues }; // changeName should be 'currentView', 'nppSortDir', or 'afcSortDir' updatedSettings[ changeName ] = changeVal; emit( 'update-settings', updatedSettings ); controlMenuOpen.value = false; };       watch( currentView, ( newVal ) => handleImmediateChange( 'currentView', newVal ) ); watch( nppSortDir, ( newVal ) => handleImmediateChange( 'nppSortDir', newVal ) ); watch( afcSortDir, ( newVal ) => handleImmediateChange( 'afcSortDir', newVal ) ); return { controlMenuOpen, toggleControlMenu, toggleControlMenuIndicator, currentView, // NPP nppSortDir, nppNamespace, nppIncludeUnreviewed, nppIncludeReviewed, nppIncludeNominated, nppIncludeRedirects, nppIncludeOthers, nppFilter, nppFilterUser, nppPredictedRating, nppPossibleIssues, nppDateFrom, nppDateTo, // AFC afcSortDir, afcSortUpdated, afcSubmissionState, afcPredictedRating, afcPossibleIssues, afcDateFrom, afcDateTo, // settings canSaveSettings, doSaveSettings };   },    //#region template template: ` <p v-if="haveDraftNamespace"> <labeled-input type="radio" input-id="mwe-vue-pt-radio-npp" v-model:inputModel="currentView" label-msg="pagetriage-new-page-patrol" value="npp" :no-break="true" /> <labeled-input type="radio" input-id="mwe-vue-pt-radio-afc" v-model:inputModel="currentView" label-msg="pagetriage-articles-for-creation" value="afc" :no-break="true" />  <span class="mwe-vue-pt-control-label-right" v-show="currentFilteredCount !== -1"> 		<labeled-input type="radio" v-model:inputModel="nppSortDir" label-msg="pagetriage-newest" value="newestfirst" :no-break="true" /> <labeled-input type="radio" v-model:inputModel="nppSortDir" label-msg="pagetriage-oldest" value="oldestfirst" :no-break="true" /> <label for="mwe-vue-pt-sort-afc"> <select v-model="afcSortDir" id="mwe-vue-pt-sort-afc"> <option v-if="afcSortUpdated" value="newestreview"> <option v-if="afcSortUpdated" value="oldestreview"> <b @click="toggleControlMenu">Set filters </b> <div class="mwe-vue-pt-control-section__row1"> <div class="mwe-vue-pt-control-section__col1"> <control-section label="pagetriage-filter-namespace-heading"> <select v-model="nppNamespace"> Article User </control-section> <control-section label="pagetriage-filter-show-heading"> <labeled-input type="checkbox" v-model:inputModel="nppIncludeUnreviewed" label-msg="pagetriage-filter-unreviewed-edits" /> <labeled-input type="checkbox" v-model:inputModel="nppIncludeReviewed" label-msg="pagetriage-filter-reviewed-edits" /> </control-section> <control-section label="pagetriage-filter-type-show-heading"> <labeled-input type="checkbox" v-model:inputModel="nppIncludeNominated" label-msg="pagetriage-filter-nominated-for-deletion" /> <labeled-input type="checkbox" v-model:inputModel="nppIncludeRedirects" label-msg="pagetriage-filter-redirects" /> <labeled-input type="checkbox" v-model:inputModel="nppIncludeOthers" label-msg="pagetriage-filter-others" /> </control-section> <template v-if="showOresFilters"> <div class="mwe-vue-pt-control-section__col2"> <control-section label="pagetriage-filter-predicted-class-heading"> <labeled-input v-for="(_, ratingName) in nppPredictedRating" :key="ratingName" type="checkbox" v-model:inputModel="nppPredictedRating[ ratingName ]" :label-msg="'pagetriage-filter-predicted-class-' + ratingName" /> </control-section> <div class="mwe-vue-pt-control-section__col3"> <control-section label="pagetriage-filter-predicted-issues-heading"> <labeled-input v-for="(_, issueName) in nppPossibleIssues" :key="issueName" type="checkbox" v-model:inputModel="nppPossibleIssues[ issueName ]" :label-msg="'pagetriage-filter-predicted-issues-' + issueName" /> </control-section> <date-control-section type="npp" v-model:fromModel="nppDateFrom" v-model:toModel="nppDateTo"></date-control-section> <template v-else> <div class="mwe-vue-pt-control-section__col2"> <date-control-section type="npp" v-model:fromModel="nppDateFrom" v-model:toModel="nppDateTo"></date-control-section> <control-section label="pagetriage-filter-second-show-heading"> <labeled-input v-for="filter in nppFilters" :key="filter" type="radio" :value="filter" v-model:inputModel="nppFilter" :label-msg="'pagetriage-filter-' + filter" /> <labeled-input type="radio" v-model:inputModel="nppFilter" label-msg="pagetriage-filter-user-heading" value="username" :no-break="true" /> <input type="text" id="mwe-vue-pt-filter-user" :placeholder="$i18n( 'pagetriage-filter-username' )" v-model="nppFilterUser"/> <labeled-input type="radio" v-model:inputModel="nppFilter" label-msg="pagetriage-filter-all" value="all" /> </control-section> <div class="mwe-vue-pt-control-section__row1"> <div class="mwe-vue-pt-control-section__col1"> <control-section label="pagetriage-filter-show-heading"> <labeled-input v-for="state in afcSubmissionStates" :key="state" type="radio" :value="state" v-model:inputModel="afcSubmissionState" :label-msg="'pagetriage-afc-state-' + state" /> </control-section> <template v-if="showOresFilters"> <date-control-section type="afc" v-model:fromModel="afcDateFrom" v-model:toModel="afcDateTo"></date-control-section> <template v-if="showOresFilters"> <div class="mwe-vue-pt-control-section__col2"> <control-section label="pagetriage-filter-predicted-class-heading"> <labeled-input v-for="(_, ratingName) in afcPredictedRating" :key="ratingName" type="checkbox" v-model:inputModel="afcPredictedRating[ ratingName ]" :label-msg="'pagetriage-filter-predicted-class-' + ratingName" /> </control-section> <div class="mwe-vue-pt-control-section__col3"> <control-section label="pagetriage-filter-predicted-issues-heading"> <labeled-input v-for="(_, issueName) in afcPossibleIssues" :key="issueName" type="checkbox" v-model:inputModel="afcPossibleIssues[ issueName ]" :label-msg="'pagetriage-filter-predicted-issues-' + issueName" /> </control-section> <template v-else> <div class="mwe-vue-pt-control-section__col2"> <date-control-section type="afc" v-model:fromModel="afcDateFrom" v-model:toModel="afcDateTo"></date-control-section> <wvui-button class="mwe-vue-pt-button-green" action="progressive" type="primary" :disabled="!canSaveSettings" @click="doSaveSettings"></wvui-button> ` }; //#endregion

/** * Convert the page information retrieved from the api into the properties * that listItemComponent expects. */ VueNPP.listItemPropFormatter = function ( pageInfo ) { // the `position` prop is handled by the list const listItemProps = {}; listItemProps.afdStatus = pageInfo.afd_status === '1'; listItemProps.blpProdStatus = pageInfo.blp_prod_status === '1'; listItemProps.csdStatus = pageInfo.csd_status === '1'; listItemProps.prodStatus = pageInfo.prod_status === '1'; listItemProps.patrolStatus = parseInt( pageInfo.patrol_status ); listItemProps.title = pageInfo.title; listItemProps.isRedirect = pageInfo.is_redirect === '1'; listItemProps.categoryCount = parseInt( pageInfo.category_count ); listItemProps.linkCount = parseInt( pageInfo.linkcount ); listItemProps.referenceCount = parseInt( pageInfo.reference ); listItemProps.recreated = !!pageInfo.recreated; listItemProps.pageLen = parseInt( pageInfo.page_len ); listItemProps.revCount = parseInt( pageInfo.rev_count ); listItemProps.creationDateUTC = pageInfo.creation_date_utc; listItemProps.creatorName = pageInfo.user_name; listItemProps.creatorAutoConfirmed = pageInfo.user_autoconfirmed === '1'; listItemProps.creatorRegistrationUTC = pageInfo.user_creation_date; listItemProps.creatorUserId = parseInt( pageInfo.user_id ); listItemProps.creatorEditCount = parseInt( pageInfo.user_editcount ); listItemProps.creatorIsBot = pageInfo.user_bot === '1'; listItemProps.creatorBlocked = pageInfo.user_block_status === '1'; listItemProps.creatorUserPageExists = pageInfo.creator_user_page_exist; listItemProps.creatorTalkPageExists = pageInfo.creator_user_talk_page_exist; listItemProps.afcState = parseInt( pageInfo.afc_state ); listItemProps.reviewedUpdatedUTC = pageInfo.ptrp_reviewed_updated; listItemProps.snippet = pageInfo.snippet; listItemProps.oresArticleQuality = pageInfo.ores_articlequality; listItemProps.oresDraftQuality = pageInfo.ores_draftquality; listItemProps.copyvio = pageInfo.copyvio || 0; return listItemProps; };

/** * Nav bar at the bottom with statistics and a refresh button. */ VueNPP.statsBarComponent = { props: { currentView: { type: String, default: 'npp' }, apiResult: { type: Object, default: => ( {} ) }   },    setup( props, { emit } ) { const triggerRefresh = => { emit( 'refresh-feed' ); };       const unreviewedCount = VueCompositionAPI.computed(  => {            if ( props.apiResult.result === 'success' && props.apiResult.stats && props.apiResult.stats.unreviewedarticle ) {               return props.apiResult.stats.unreviewedarticle.count;            }            // Should not be shown            return -1;        } ); const unreviewedOldest = VueCompositionAPI.computed( => {            if ( props.apiResult.result === 'success' && props.apiResult.stats && props.apiResult.stats.unreviewedarticle ) {               const rawOldest = props.apiResult.stats.unreviewedarticle.oldest;                // convert to number of days based on formatDaysFromNow in                // pagetriage                if ( !rawOldest ) {                    return '';                }                var now = new Date;                now = new Date( now.getUTCFullYear, now.getUTCMonth, now.getUTCDate, now.getUTCHours, now.getUTCMinutes, now.getUTCSeconds );               var begin = moment.utc( rawOldest, 'YYYYMMDDHHmmss' );                var diff = Math.round( ( now.getTime - begin.valueOf ) / ( 1000 * 60 * 60 * 24 ) );                if ( diff ) {                    return mw.msg( 'days', diff );                }				return mw.msg( 'pagetriage-stats-less-than-a-day', diff );            }            // Should not be shown            return '?';        } ); const reviewedCount = VueCompositionAPI.computed( => {            if ( props.apiResult.result === 'success' && props.apiResult.stats && props.apiResult.stats.reviewedarticle ) {               return props.apiResult.stats.reviewedarticle.reviewed_count;            }            // Should not be shown            return -1;        } ); const showStats = VueCompositionAPI.computed( => {            // make sure all the values were computed            return props.currentView === 'npp'                && unreviewedCount.value !== -1                && unreviewedOldest.value !== '?'                && reviewedCount.value !== -1        } ); return { triggerRefresh, showStats, unreviewedCount, unreviewedOldest, reviewedCount };   },    template: ` <button id="mwe-vue-pt-refresh-button" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" @click="triggerRefresh">

` };

/** * Component for the bar after the last entry that allows loading more when * scrolled into view. Whether to show or not is based on a prop instead of * being controlled in the calling code so that the intersection observer * does not need to be recreated each time. */ VueNPP.loadMoreBarComponent = { props: { haveMore: { type: Boolean, required: true } },   setup( props, { emit } ) { const emitLoadMore = function { // check that we should try to load if ( props.haveMore ) { emit( 'trigger-load' ); }       };        const barRef = VueCompositionAPI.ref; const observerCallback = function ( entries, observer ) { const observerEntry = entries[0]; // whether we scrolled to see it or away from it           const nowSeen = observerEntry.isIntersecting; if ( !nowSeen ) { return; }           // console.log( observerEntry ); emitLoadMore; };       const observer = new IntersectionObserver( observerCallback ); Vue.onMounted( => {            observer.observe( barRef.value );        } ); return { barRef, emitLoadMore };   },    template: ` <wvui-progress-bar></wvui-progress-bar> <wvui-button action="progressive" type="quiet" @click="emitLoadMore">Load more</wvui-button> ` };

/** * Component for the overall list contents, is given the api properties to * query with and generates the items to show. */ VueNPP.feedContentsComponent = { // wvui components are added separately components: { listItem: VueNPP.listItemComponent, loadMoreBar: VueNPP.loadMoreBarComponent, statsBar: VueNPP.statsBarComponent },	props: { params: { type: Object, required: true } },	data: function { return { // Enable adding by specific page id for debugging manualDebug: false };	},	setup( props, { emit } ) { const API_PAGE_LIMIT = 20; const ref = VueCompositionAPI.ref;

const api = new mw.ForeignApi( '//en.wikipedia.org/w/api.php' ); const apiError = ref( false ); const feedEntries = ref( [] ); // incremented before being used const latestPosition = ref( 0 ); // 0 is ignored; `offset` and `pageoffset` parameters const apiOffsets = ref( { normal: 0, page: 0 } ); const haveMoreToLoad = ref( true );

const alreadyLoading = ref( false ); const onApiFailure = function ( res, shouldRender ) { console.log( res ); if ( shouldRender ) { apiError.value = true; }           alreadyLoading.value = false; };       const addPageToFeed = function ( pageInfo ) { const propData = VueNPP.listItemPropFormatter( pageInfo ); propData.position = ( ++latestPosition.value ); feedEntries.value.push( propData ); };		const processResult = function ( res ) { // console.log( res ); if ( !res || !res.pagetriagelist || !res.pagetriagelist.pages				|| !res.pagetriagelist.pages[0]			) { onApiFailure( res, true ); return; }			haveMoreToLoad.value = false; const allPages = res.pagetriagelist.pages; if ( allPages.length > API_PAGE_LIMIT ) { // Have more to load allPages.pop; haveMoreToLoad.value = true; }			for ( var iii = 0; iii < allPages.length; iii++ ) { addPageToFeed( allPages[iii] ); }			// offset with the last const lastPage = allPages[ allPages.length - 1 ]; apiOffsets.value.normal = lastPage.creation_date_utc; apiOffsets.value.page = lastPage.pageid; alreadyLoading.value = false; };		const addFromApi = function ( apiParams ) { apiParams.action = 'pagetriagelist'; apiParams.format = 'json'; apiParams.formatversion = 2; apiParams.limit = API_PAGE_LIMIT; apiParams.offset = apiOffsets.value.normal; apiParams.pageoffset = apiOffsets.value.page; // console.log( apiParams ); api.get( apiParams ).then(		       ( res ) => processResult( res ),		        ( res ) => onApiFailure( res, true )		    ); };

// Default is FDP Hamburg for now (for manualDebug) const targetPageId = ref( 70853005 ); const updatePageId = ( newPageId ) => targetPageId.value = newPageId; const addFromPageId = function { addFromApi( { page_id: targetPageId.value } ); };		const loadFromFilters = function { if ( alreadyLoading.value === true ) { // race condition return; }		   alreadyLoading.value = true; console.log( 'Loading from filters' ); // make a copy, and remove unknown param const paramsFromProps = { ...props.params }; delete paramsFromProps.mode; addFromApi( paramsFromProps ); };		// Passed to stats bar const currentView = VueCompositionAPI.computed( => {		    return props.params.mode;		} ); const clearCurrentData = function { feedEntries.value = []; latestPosition.value = 0; haveMoreToLoad.value = true; apiOffsets.value.normal = 0; apiOffsets.value.page = 0; };		const feedStats = ref( {} ); const processNewStats = function ( newStats ) { // console.log( newStats ); feedStats.value = newStats.pagetriagestats; // hack - the number of pages in the filtered list is used in a		   // different component (the menu bar at the top) and its easier // to fetch the stats here than to fetch in the parent, send the // data up via events emit( 'new-filtered-count', newStats.pagetriagestats.stats.filteredarticle ); };		const updateStats = function { // make a copy, and remove unknown params const apiParams = { ...props.params }; delete apiParams.mode; delete apiParams.dir; apiParams.action = 'pagetriagestats'; apiParams.format = 'json'; apiParams.formatversion = 2; // console.log( apiParams ); api.get( apiParams ).then(		       ( res ) => processNewStats( res ),		        ( res ) => onApiFailure( res, false )		    ); }	   const refreshFeed = function  { console.log( 'Should refresh feed' ); clearCurrentData; loadFromFilters; updateStats; }		VueCompositionAPI.watch(		   VueCompositionAPI.toRef( props, 'params' ),		    refreshFeed	    ); Vue.onMounted( => refreshFeed );

return { targetPageId, updatePageId, addFromPageId, apiError, feedEntries, haveMoreToLoad, loadFromFilters, refreshFeed, currentView, feedStats };	},	template: ` Specific page entry, by page id: <wvui-input :value="targetPageId" v-on:input="updatePageId"></wvui-input> <wvui-button action="progressive" type="primary" v-on:click="addFromPageId">Add entry</wvui-button>

Api error, see console

<template v-if="feedEntries"> <list-item v-for="feedEntry in feedEntries" :key="feedEntry.position" v-bind="feedEntry"></list-item> <load-more-bar :have-more="haveMoreToLoad" @trigger-load="loadFromFilters"></load-more-bar>

<stats-bar :current-view="currentView" :api-result="feedStats" @refresh-feed=refreshFeed></stats-bar> ` };

/** * Interface for user to choose an article */ VueNPP.NPPFeedMenu = { // wvui components are added separately components: { feedControlMenu: VueNPP.feedControlMenuComponent, feedContents: VueNPP.feedContentsComponent },	setup( props ) { const ref = VueCompositionAPI.ref; const computed = VueCompositionAPI.computed;

const currentSettings = ref ( {           currentView: 'npp',            nppSortDir: 'newestfirst',            nppNamespace: 0,            nppIncludeUnreviewed: true,            nppIncludeReviewed: true,            nppIncludeNominated: true,            nppIncludeRedirects: false,            nppIncludeOthers: true,			nppFilter: 'all',			nppFilterUser: ,			nppPredictedRating: {                stub: false,                start: false,                c: false,                b: false,                good: false,                featured: false			},			nppPossibleIssues: {			    vandalism: false,                spam: false,                attack: false,                copyvio: false,                none: false			},			nppDateFrom: ,			nppDateTo: '',			afcSortDir: 'newestfirst',			afcSubmissionState: 'all',			afcPredictedRating: {                stub: false,                start: false, c: false, b: false, good: false, featured: false },			afcPossibleIssues: { vandalism: false, spam: false, attack: false, copyvio: false, none: false },			afcDateFrom: '', afcDateTo: '', } );       const updateSettings = function ( newVal ) {            // deep copy             currentSettings.value = newVal;            currentSettings.value.nppPredictedRating = { ...newVal.nppPredictedRating };            currentSettings.value.nppPossibleIssues = { ...newVal.nppPossibleIssues };            currentSettings.value.afcPredictedRating = { ...newVal.afcPredictedRating };            currentSettings.value.afcPossibleIssues = { ...newVal.afcPossibleIssues };        };        const offset = parseInt( mw.user.options.get( 'timecorrection' ).split( '|' )[ 1 ] );        const apiOptions = computed(  => { // shortcut const currentSV = currentSettings.value; // limit is added by feedContentsComponent const params = { mode: currentSV.currentView };           const addIfToggled = function ( paramName, optionToggle ) { if ( optionToggle ) { params[ paramName ] = '1'; }           };    	    const addOresFilters = function ( optionsObj, paramPrefix ) { for ( var optionName in optionsObj ) { addIfToggled( paramPrefix + optionName, optionsObj[ optionName ] ); }           };            const addNppFilter = function  { const filtersToParams = { 'no-categories': 'no_category', 'unreferenced': 'unreferenced', 'orphan': 'no_inbound_links', 'recreated': 'recreated', 'non-autoconfirmed': 'non_autoconfirmed_users', 'learners': 'learners', 'blocked': 'blocked_users', 'bot-edits': 'showbots' };               const chosenFilter = currentSV.nppFilter; if ( chosenFilter === 'username' && currentSV.nppFilterUser ) { params.username = currentSV.nppFilterUser; // if username is chosen with no filter, or 'all' } else if ( filtersToParams[ chosenFilter ] !== undefined ) { params[ filtersToParams[ chosenFilter ] ] = '1'; }           };            const addDateParams = function ( fromVal, toVal ) { if ( fromVal ) { const fromDate = moment.utc( fromVal ).subtract( offset, 'minutes' ); params.date_range_from = fromDate.toISOString; }               if ( toVal ) { let toDate = moment.utc( toVal ).subtract( offset, 'minutes' ); // move to the end of the given day toDate.add( 1, 'day' ).subtract( 1, 'second' ); params.date_range_to = toDate.toISOString; }           };            if ( currentSV.currentView === 'npp' ) { addIfToggled( 'showreviewed', currentSV.nppIncludeReviewed ); addIfToggled( 'showunreviewed', currentSV.nppIncludeUnreviewed ); addIfToggled( 'showdeleted', currentSV.nppIncludeNominated ); addIfToggled( 'showredirs', currentSV.nppIncludeRedirects ); addIfToggled( 'showothers', currentSV.nppIncludeOthers ); addNppFilter; addOresFilters( currentSV.nppPredictedRating, 'show_predicted_class_' ); addOresFilters( currentSV.nppPossibleIssues, 'show_predicted_issues_' ); params.namespace = currentSV.nppNamespace; params.dir = currentSV.nppSortDir; addDateParams( currentSV.nppDateFrom, currentSV.nppDateTo ); } else { addOresFilters( currentSV.afcPredictedRating, 'show_predicted_class_' ); addOresFilters( currentSV.afcPossibleIssues, 'show_predicted_issues_' ); params.showreviewed = '1'; params.showunreviewed = '1'; params.namespace = 118 || mw.config.get( 'wgNamespaceIds' ).draft; params.dir = currentSV.afcSortDir; const afcSubmissionStateApi = VueNPP.getAfcStateForApi( currentSV.afcSubmissionState ); if ( afcSubmissionStateApi !== false ) { params.afc_state = afcSubmissionStateApi; }               addDateParams( currentSV.afcDateFrom, currentSV.afcDateTo ); }           return params; } );       const showingText = computed(  => { const showingMessageObj = { namespace: [], state: [], type: [], 'predicted-class': [], 'predicted-issues': [], top: [], date_range: [] };           const addOresShowing = function ( settingsObj, msgPrefix ) { for ( var settingsOption in settingsObj ) { if ( settingsObj[ settingsOption ] ) { showingMessageObj[ msgPrefix ].push(                           mw.msg( `pagetriage-filter-stat-${msgPrefix}-${settingsOption}` )                        ); }               }            };            const addDateShowing = function ( dateFrom, dateTo ) { if ( dateFrom ) { const forFormattingFrom = moment( dateFrom ); const formattedFrom = forFormattingFrom.utcOffset( offset ) .format( mw.msg( 'pagetriage-filter-date-range-format-showing' ) ); showingMessageObj.date_range.push(                       mw.msg( 'pagetriage-filter-stat-date_range_from', formattedFrom )                    ); }               if ( dateTo ) { const forFormattingTo = moment( dateTo ); const formattedTo = forFormattingTo.utcOffset( offset ) .format( mw.msg( 'pagetriage-filter-date-range-format-showing' ) ); showingMessageObj.date_range.push(                       mw.msg( 'pagetriage-filter-stat-date_range_to', formattedTo )                    ); }           };            const addShowingIf = function ( isApplicable, msgSuffix, msgGroup ) { if ( isApplicable ) { showingMessageObj[ msgGroup ].push(                       mw.msg( `pagetriage-filter-stat-${msgSuffix}` )                    ); }           };            const currentSV = currentSettings.value; if ( currentSV.currentView === 'npp' ) { showingMessageObj.namespace.push(                   currentSV.nppNamespace === 0 ? 'Article' : 'User'                ); const showingNPPFilter = currentSV.nppFilter; if ( showingNPPFilter === 'username' ) { if ( currentSV.nppFilterUser ) { showingMessageObj.top.push(                           mw.msg( 'pagetriage-filter-stat-username', currentSV.nppFilterUser )                        ); }               } else if ( showingNPPFilter === 'bot-edits' ) { // Need a different message key (not -bot-edits) showingMessageObj.top.push( mw.msg( 'pagetriage-filter-stat-bots' ) ); } else if ( showingNPPFilter !== 'all' ) { showingMessageObj.top.push( mw.msg( `pagetriage-filter-stat-${showingNPPFilter}` ) ); }               addShowingIf( currentSV.nppIncludeReviewed, 'reviewed', 'state' ); addShowingIf( currentSV.nppIncludeUnreviewed, 'unreviewed', 'state' ); addShowingIf( currentSV.nppIncludeNominated, 'nominated-for-deletion', 'type' ); addShowingIf( currentSV.nppIncludeRedirects, 'redirects', 'type' ); addShowingIf( currentSV.nppIncludeOthers, 'others', 'type' ); addOresShowing( currentSV.nppPredictedRating, 'predicted-class' ); addOresShowing( currentSV.nppPossibleIssues, 'predicted-issues' ); addDateShowing( currentSV.nppDateFrom, currentSV.nppDateTo ); } else { addOresShowing( currentSV.afcPredictedRating, 'predicted-class' ); addOresShowing( currentSV.afcPossibleIssues, 'predicted-issues' ); addDateShowing( currentSV.afcDateFrom, currentSV.afcDateTo ); showingMessageObj.state.push(                   mw.msg( `pagetriage-afc-state-${currentSV.afcSubmissionState}` )                ); }           return Object.keys( showingMessageObj ) .map( function ( group ) {                   const groupShowing = showingMessageObj[ group ];                    if ( groupShowing.length === 0 ) {                        return '';                    }                    if ( group == 'top' || ( currentSV.currentView === 'afc' && group === 'state' ) ) {                        return groupShowing[ 0 ];                    }                    return mw.msg( `pagetriage-filter-stat-${group}` ) + ' '                        + mw.msg( 'parentheses', groupShowing.join( mw.msg( 'comma-separator' ) ) );                } ) .filter( ( msg ) => msg !== '' ) .join( mw.msg( 'comma-separator' ) ); } );

// start as -1 until fetched the first time; fetched with the rest of       // the statistics in the nav bar within feed contents, and then // passed up via an event to all knowing it here to pass to the menu const currentFilteredCount = ref( -1 ); const updateFilteredCount = function ( val ) { currentFilteredCount.value = val; };	   return { currentSettings, updateSettings, apiOptions, showingText, currentFilteredCount, updateFilteredCount };	},	template: ` Settings: <feed-control-menu :start-options="currentSettings" @update-settings="updateSettings" :currently-showing-text="showingText" :current-filtered-count="currentFilteredCount"></feed-control-menu>

<feed-contents :params="apiOptions" @new-filtered-count="updateFilteredCount"></feed-contents> ` };

//#endregion

//module.exports = VueNPP.NPPFeedMenu;

/** * Render VueNPP interface */ VueNPP.renderInterface = function { Vue.createMwApp( VueNPP.NPPFeedMenu ) .mount( '#mw-content-text' ); };

});

$( document ).ready( => {	if ( mw.config.get( 'wgPageName' ) === 'Special:BlankPage/VueNPP' ) {		window.VueNPP.init;	} });

//