User:DannyS712/VueNPP.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:DannyS712/VueNPP. |
// <nowiki>
// Script to experiment with a Vue version of Special:NewPagesFeed
// @author DannyS712
/* jshint maxerr: 999 */
$(() => {
const VueNPP = {};
window.VueNPP = VueNPP;
VueNPP.init = function () {
mw.loader.using(
[
'vue',
'@vue/composition-api',
'wvui',
'mediawiki.util',
'mediawiki.api',
'es6-polyfills',
'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;
// Object.assign for the components that have other non-wvui components
Object.assign( VueNPP.feedControlMenuComponent.components, wvuiComponents );
Object.assign( VueNPP.NPPFeedMenu.components, wvuiComponents );
VueNPP.addStyle();
VueNPP.renderInterface();
};
/**
* Add styles for our interface.
*/
VueNPP.addStyle = function () {
mw.util.addCSS(`
.mwe-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;
}
#mwe-vue-pt-list-control-nav-content {
padding: 0.5em 1em 1em 1em;
}
.stickyTop #mwe-vue-pt-list-control-nav {
position: fixed;
top: 0;
z-index: 1;
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;
}
#mwe-vue-pt-control-dropdown {
top: 23px;
left: 48px;
border: 1px solid #aaa;
padding: 0.5em 1em 0.2em 1em;
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;
}
#mwe-vue-pt-filter-user {
width: 100px;
}
#mwe-vue-pt-sort-buttons {
margin-right: 0.3em;
}
#mwe-vue-pt-radio-afc {
margin-left: 10px;
}
`);
};
/**
* 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 = $( '<a>' );
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-pt-article-row-even' : 'mwe-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 );
},
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: `<div class="mwe-pt-article-row" :class="oddEvenClass">
<div class="mwe-pt-status-icon">
<img :src="iconImageSrc" width="21" height="21" />
</div>
<div class="mwe-pt-info-pane">
<div class="mwe-pt-info-row">
<div class="mwe-pt-article">
<span class="mwe-pt-page-title"><a :href="titleUrl" target="_blank">{{ title }}</a></span>
<span class="mwe-pt-histlink">
(<a :href="historyUrl">{{ $i18n( "pagetriage-hist" ) }}</a>)
</span>
<span class="mwe-pt-metadata">
·
{{ $i18n( "pagetriage-bytes", pageLen ) }}
·
{{ $i18n( "pagetriage-edits", revCount ) }}
<span v-if="!isDraft">
<span v-if="categoryCount === 0 && !isRedirect" class="mwe-pt-metadata-warning">{{ $i18n( "pagetriage-no-categories" ) }}</span>
<template v-if="categoryCount !== 0">
· {{ $i18n( "pagetriage-categories", categoryCount ) }}
</template>
<span v-if="linkCount === 0 && !isRedirect" class="mwe-pt-metadata-warning">{{ $i18n("pagetriage-orphan") }}</span>
<span v-if="recreated" class="mwe-pt-metadata-warning">{{ $i18n("pagetriage-recreated") }}</span>
</span>
<span v-if="referenceCount === 0 && !isRedirect" class="mwe-pt-metadata-warning">{{ $i18n( "pagetriage-no-reference" ) }}</span>
</span>
</div>
<span class="mwe-pt-creation-date">{{ creationDatePretty }}</span>
</div>
<div class="mwe-pt-info-row">
<div class="mwe-pt-author">
<span v-if="creatorName">
<!-- Using v-html because the messages used embed links within them -->
<span v-if="creatorBylineHtml" v-html="creatorBylineHtml"></span>
<span v-if="creatorUserId > 0">
·
{{ $i18n( 'pagetriage-editcount', creatorEditCount, creatorRegistrationPretty ) }}
<span v-if="creatorIsBot">
·
{{ $i18n( 'pagetriage-author-bot' ) }}
</span>
</span>
<span v-if="creatorBlocked" class="mwe-pt-metadata-warning">{{ $i18n( 'pagetriage-author-blocked' ) }}</span>
</span>
<span v-else>
{{ $i18n('pagetriage-no-author') }}
</span>
</div>
<div class="mwe-pt-updated-date">
<span v-if="lastAfcAction">
<span>{{ $i18n( lastAfcAction ) }}</span>
<span>{{ reviewedUpdatedPretty }}</span>
</span>
</div>
</div>
<div class="mwe-pt-info-row">
<div class="mwe-pt-snippet">{{ snippet }}</div>
<div class="mwe-pt-review">
<a class="mwe-pt-list-triage-button" :href="titleUrl" target="_blank" :title="reviewRightHelpText">
<wvui-button action="progressive" type="primary">Review</wvui-button>
</a>
</div>
</div>
<div v-if="showOres" class="mwe-pt-info-row">
<div class="mwe-pt-predicted-class">
<span class="mwe-pt-article-ores-predicted-class-label">
{{ $i18n( 'pagetriage-filter-predicted-class-heading' ) }}
</span>
<span class="mwe-pt-article-ores-predicted-class-value">{{ oresArticleQuality }}</span>
</div>
<div class="mwe-pt-potential-issues">
<span>{{ $i18n( 'pagetriage-filter-predicted-issues-heading' ) }}</span>
<span v-if="!oresDraftQuality && !( copyvio && showCopyvio )">
{{ $i18n( 'pagetriage-filter-stat-predicted-issues-none' ) }}
</span>
<span v-if="oresDraftQuality" class="mwe-pt-issue">{{ oresDraftQuality }}</span>
<span v-if="copyvio && showCopyvio">
<span v-if="oresDraftQuality">·</span>
<span class="mw-parser-output mwe-pt-issue">
<a :href="copyvioLink" target="_blank" class="external">
{{ $i18n( 'pagetriage-filter-stat-predicted-issues-copyvio' ) }}
</a>
</span>
</span>
</div>
</div>
</div>
</div>`
};
/**
* Helper for controls form, contains a specific section with a message label
* and slot content
*/
VueNPP.controlSectionComponent = {
props: { label: { type: String, required: true } },
template: `
<div class="mwe-pt-control-section">
<span class="mwe-pt-control-label">
<b>{{ $i18n( label ) }}</b>
</span>
<div class="mwe-pt-control-options">
<slot></slot>
</div>
</div>`
};
/**
* 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-pt-filter-' + type + '-date-range-from'">{{ $i18n( 'pagetriage-filter-date-range-from' ) }}</label>
<input type="date" :value="fromModel" @input="updateFrom" :id="'mwe-pt-filter-' + type + '-date-range-from'" :placeholder="$i18n( 'pagetriage-filter-date-range-format-placeholder' )" /> <br/>
<label :for="'mwe-pt-filter-' + type + '-date-range-to'">{{ $i18n( 'pagetriage-filter-date-range-to' ) }}</label>
<input type="date" :value="toModel" @input="updateTo" :id="'mwe-pt-filter-' + type + '-date-range-to'" :placeholder="$i18n( 'pagetriage-filter-date-range-format-placeholder' )" />
</control-section>`
};
/**
* Menu for controlling the filters for the pages feed
*/
VueNPP.feedControlMenuComponent = {
components: {
controlSection: VueNPP.controlSectionComponent,
dateControlSection: VueNPP.dateControlSectionComponent
},
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 ) {
// Shortcuts
const ref = VueCompositionAPI.ref;
const computed = VueCompositionAPI.computed;
const currentView = ref( 'npp' );
//#region NPP
const nppSortDir = ref( 'newestfirst' );
const nppNamespace = ref( 0 );
// Review status
const nppIncludeUnreviewed = ref( true );
const nppIncludeReviewed = ref( true );
// Nominated for deletion / redirects / normal article status
const nppIncludeNominated = ref( true );
const nppIncludeRedirects = ref( false );
const nppIncludeOthers = ref( true );
// Filters for article creator or problems
const nppFilter = ref( 'all' );
const nppFilterUser = ref( '' );
// Filters for rating and issues
const nppPredictedRating = ref( {
stub: false,
start: false,
c: false,
b: false,
good: false,
featured: false
} );
const nppPossibleIssues = ref( {
vandalism: false,
spam: false,
attack: false,
copyvio: false,
none: false
} );
const nppDateFrom = ref( '' );
const nppDateTo = ref( '' );
//#endregion
//#region AFC
const afcSortDir = ref( 'newestfirst' );
// Filter for submission state
const afcSubmissionState = ref( 'all' );
// 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';
} else {
return false;
}
} );
// Make sure that afcSortDir isn't invalid
VueCompositionAPI.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';
}
}
);
// convert to integer used by api
const afcSubmissionStateApi = computed( () => {
// PageTriage extension uses literal 'all' which breaks things,
// use `false` so that mw.Api() filters it out, T304574
const submissionNumbers = [ '~invalid~', 'unsubmitted', 'pending', 'reviewing', 'declined' ];
const stateIndex = submissionNumbers.indexOf( afcSubmissionState.value );
return ( stateIndex <= 0 ? false : stateIndex.toString() );
} );
// Filters for rating and issues
const afcPredictedRating = ref( {
stub: false,
start: false,
c: false,
b: false,
good: false,
featured: false
} );
const afcPossibleIssues = ref( {
vandalism: false,
spam: false,
attack: false,
copyvio: false,
none: false
} );
const afcDateFrom = ref( '' );
const afcDateTo = ref( '' );
//#endregion
// 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
)
);
} );
const addOresFilters = function ( paramsObj, optionsRef, paramPrefix ) {
for ( var optionName in optionsRef.value ) {
if ( optionsRef.value[ optionName ] ) {
paramsObj[ paramPrefix + optionName ] = '1';
}
}
};
const addIfToggled = function ( paramsObj, paramName, optionRef ) {
if ( optionRef.value ) {
paramsObj[ paramName ] = '1';
}
};
const addNppFilter = function ( paramsObj ) {
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 = nppFilter.value;
if ( nppFilter.value === 'username' && nppFilterUser.value ) {
paramsObj.username = nppFilterUser.value;
// if username is chosen with no filter, or 'all'
} else if ( filtersToParams[ chosenFilter ] !== undefined ) {
paramsObj[ filtersToParams[ chosenFilter ] ] = '1';
}
};
const offset = parseInt( mw.user.options.get( 'timecorrection' ).split( '|' )[ 1 ] );
const addDateParams = function ( paramsObj, fromRef, toRef ) {
if ( fromRef.value ) {
const fromDate = moment.utc( fromRef.value ).subtract( offset, 'minutes' );
paramsObj.date_range_from = fromDate.toISOString();
}
if ( toRef.value ) {
let toDate = moment.utc( toRef.value ).subtract( offset, 'minutes' );
// end of the day
toDate.add( 1, 'day' ).subtract( 1, 'second' );
paramsObj.date_range_to = toDate.toISOString();
}
};
const apiOptions = computed( () => {
const params = {
mode: currentView.value,
nppDir: nppSortDir.value,
afcDir: afcSortDir.value,
limit: 20
};
if ( currentView.value === 'npp' ) {
addIfToggled( params, 'showreviewed', nppIncludeReviewed );
addIfToggled( params, 'showunreviewed', nppIncludeUnreviewed );
addIfToggled( params, 'showdeleted', nppIncludeNominated );
addIfToggled( params, 'showredirs', nppIncludeRedirects );
addIfToggled( params, 'showothers', nppIncludeOthers );
addNppFilter( params );
addOresFilters( params, nppPredictedRating, 'show_predicted_class_' );
addOresFilters( params, nppPossibleIssues, 'show_predicted_issues_' );
params.namespace = nppNamespace.value;
params.dir = params.nppDir;
addDateParams( params, nppDateFrom, nppDateTo );
} else {
addOresFilters( params, afcPredictedRating, 'show_predicted_class_' );
addOresFilters( params, afcPossibleIssues, 'show_predicted_issues_' );
params.showreviewed = '1';
params.showunreviewed = '1';
params.namespace = 118 || mw.config.get( 'wgNamespaceIds' ).draft;
params.dir = params.afcDir;
if ( afcSubmissionStateApi.value !== false ) {
params.afc_state = afcSubmissionStateApi.value;
}
addDateParams( params, afcDateFrom, afcDateTo );
}
return params;
} );
return {
currentView,
apiOptions,
// NPP
nppSortDir,
nppNamespace,
nppIncludeUnreviewed, nppIncludeReviewed,
nppIncludeNominated, nppIncludeRedirects, nppIncludeOthers,
nppFilter, nppFilterUser,
nppPredictedRating, nppPossibleIssues,
nppDateFrom, nppDateTo,
// AFC
afcSortDir, afcSortUpdated,
afcSubmissionState,
afcPredictedRating, afcPossibleIssues,
afcDateFrom, afcDateTo,
canSaveSettings
};
},
template: `<div>
{{ apiOptions }}
<p v-if="haveDraftNamespace">
<input type="radio" id="mwe-vue-pt-radio-npp" value="npp" v-model="currentView"/>
<label for="mwe-vue-pt-radio-npp">{{ $i18n( 'pagetriage-new-page-patrol' ) }}</label>
<input type="radio" id="mwe-vue-pt-radio-afc" value="afc" v-model="currentView"/>
<label for="mwe-vue-pt-radio-afc">{{ $i18n( 'pagetriage-articles-for-creation' ) }}</label>
</p>
<span class="mwe-vue-pt-control-label">
<b>{{ $i18n( 'pagetriage-showing' ) }} </b>
<span id="mwe-vue-pt-filter-status"></span>
</span>
<span class="mwe-vue-pt-control-label-right" id="mwe-vue-pt-control-stats"></span><br/>
<span v-show="currentView === 'npp'" class="mwe-vue-pt-control-label-right"><b>{{ $i18n( 'pagetriage-sort-by' ) }} </b>
<span id="mwe-vue-pt-sort-buttons">
<input type="radio" id="mwe-vue-pt-sort-newest" value="newestfirst" v-model="nppSortDir"/>
<label for="mwe-vue-pt-sort-newest">{{ $i18n( 'pagetriage-newest' ) }}</label>
<input type="radio" id="mwe-vue-pt-sort-oldest" value="oldestfirst" v-model="nppSortDir"/>
<label for="mwe-vue-pt-sort-oldest">{{ $i18n( 'pagetriage-oldest' ) }}</label>
</span>
</span>
<span v-show="currentView === 'afc'" class="mwe-vue-pt-control-label-right">
<label for="mwe-vue-pt-sort-afc">
<b>{{ $i18n( 'pagetriage-sort-by' ) }}</b>
</label>
<select v-model="afcSortDir" id="mwe-vue-pt-sort-afc">
<option value="newestfirst">{{ $i18n( 'pagetriage-afc-newest' ) }}</option>
<option value="oldestfirst">{{ $i18n( 'pagetriage-afc-oldest' ) }}</option>
<!--
'newestreview' and 'oldestreview' are used for both newest/oldest submitted and newest/oldest declined,
PageTriage adds one or the other, we just change the label - only shown when filtering for submitted, under review, or declined
-->
<option v-if="afcSortUpdated" value="newestreview">{{ $i18n( 'pagetriage-afc-newest-' + afcSortUpdated ) }}</option>
<option v-if="afcSortUpdated" value="oldestreview">{{ $i18n( 'pagetriage-afc-oldest-' + afcSortUpdated ) }}</option>
</select>
</span>
<div id="mwe-vue-pt-control-dropdown" class="mwe-vue-pt-control-gradient">
<div v-show="currentView === 'npp'" class="mwe-vue-pt-control-section__npp">
<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">
<option value="0">Article</option>
<option value="2">User</option>
</select>
</control-section>
<control-section label="pagetriage-filter-show-heading">
<input type="checkbox" id="mwe-vue-pt-filter-unreviewed-edits" v-model="nppIncludeUnreviewed" />
<label for="mwe-vue-pt-filter-unreviewed-edits">{{ $i18n( 'pagetriage-filter-unreviewed-edits' ) }}</label> <br/>
<input type="checkbox" id="mwe-vue-pt-filter-reviewed-edits" v-model="nppIncludeReviewed" />
<label for="mwe-vue-pt-filter-reviewed-edits">{{ $i18n( 'pagetriage-filter-reviewed-edits' ) }}</label> <br/>
</control-section>
<control-section label="pagetriage-filter-type-show-heading">
<input type="checkbox" id="mwe-vue-pt-filter-nominated-for-deletion" v-model="nppIncludeNominated" />
<label for="mwe-vue-pt-filter-nominated-for-deletion">{{ $i18n( 'pagetriage-filter-nominated-for-deletion' ) }}</label> <br/>
<input type="checkbox" id="mwe-vue-pt-filter-redirects" v-model="nppIncludeRedirects" />
<label for="mwe-vue-pt-filter-redirects">{{ $i18n( 'pagetriage-filter-redirects' ) }}</label> <br/>
<input type="checkbox" id="mwe-vue-pt-filter-others" v-model="nppIncludeOthers" />
<label for="mwe-vue-pt-filter-others">{{ $i18n( 'pagetriage-filter-others' ) }}</label> <br/>
</control-section>
</div>
<template v-if="showOresFilters">
<div class="mwe-vue-pt-control-section__col2">
<control-section label="pagetriage-filter-predicted-class-heading">
<template v-for="(_, ratingName) in nppPredictedRating" :key="ratingName">
<input type="checkbox" :id="'mwe-vue-pt-filter-npp-predicted-class-' + ratingName" v-model="nppPredictedRating[ ratingName ]" />
<label :for="'mwe-vue-pt-filter-npp-predicted-class-' + ratingName">{{ $i18n( 'pagetriage-filter-predicted-class-' + ratingName ) }}</label> <br/>
</template>
</control-section>
</div>
<div class="mwe-vue-pt-control-section__col3">
<control-section label="pagetriage-filter-predicted-issues-heading">
<template v-for="(_, issueName) in nppPossibleIssues" :key="issueName">
<input type="checkbox" :id="'mwe-vue-pt-filter-npp-predicted-issues-' + issueName" v-model="nppPossibleIssues[ issueName ]" />
<label :for="'mwe-vue-pt-filter-npp-predicted-issues-' + issueName">{{ $i18n( 'pagetriage-filter-predicted-issues-' + issueName ) }}</label> <br/>
</template>
</control-section>
<date-control-section type="npp" v-model:fromModel="nppDateFrom" v-model:toModel="nppDateTo"></date-control-section>
</div>
</template>
<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>
</div>
</template>
</div>
<control-section label="pagetriage-filter-second-show-heading">
<template v-for="filter in nppFilters" :key="filter">
<input type="radio" name="mwe-vue-pt-filter-radio" :id="'mwe-vue-pt-filter-' + filter" :value="filter" v-model="nppFilter"/>
<label :for="'mwe-vue-pt-filter-' + filter">{{ $i18n( 'pagetriage-filter-' + filter ) }}</label> <br/>
</template>
<input type="radio" name="mwe-vue-pt-filter-radio" id="mwe-vue-pt-filter-user-selected" value="username" v-model="nppFilter"/>
<label for="mwe-vue-pt-filter-user-selected">{{ $i18n( 'pagetriage-filter-user-heading' ) }}</label>
<input type="text" id="mwe-vue-pt-filter-user" :placeholder="$i18n( 'pagetriage-filter-username' )" v-model="nppFilterUser"/> <br/>
<input type="radio" name="mwe-vue-pt-filter-radio" id="mwe-vue-pt-filter-all" value="all" v-model="nppFilter"/>
<label for="mwe-vue-pt-filter-all">{{ $i18n( 'pagetriage-filter-all' ) }}</label>
</control-section>
</div>
<div v-show="currentView === 'afc'" class="mwe-vue-pt-control-section__afc">
<div class="mwe-vue-pt-control-section__row1">
<div class="mwe-vue-pt-control-section__col1">
<control-section label="pagetriage-filter-show-heading">
<template v-for="state in afcSubmissionStates" :key="state">
<input type="radio" name="mwe-vue-pt-filter-afc-radio" :id="'mwe-vue-pt-filter-afc-' + state" :value="state" v-model="afcSubmissionState" />
<label :for="'mwe-vue-pt-filter-afc-' + state"> {{ $i18n( 'pagetriage-afc-state-' + state ) }} </label> <br/>
</template>
</control-section>
<template v-if="showOresFilters">
<date-control-section type="afc" v-model:fromModel="afcDateFrom" v-model:toModel="afcDateTo"></date-control-section>
</template>
</div>
<template v-if="showOresFilters">
<div class="mwe-vue-pt-control-section__col2">
<control-section label="pagetriage-filter-predicted-class-heading">
<template v-for="(_, ratingName) in afcPredictedRating" :key="ratingName">
<input type="checkbox" :id="'mwe-vue-pt-filter-afc-predicted-class-' + ratingName" v-model="afcPredictedRating[ ratingName ]" />
<label :for="'mwe-vue-pt-filter-afc-predicted-class-' + ratingName">{{ $i18n( 'pagetriage-filter-predicted-class-' + ratingName ) }}</label> <br/>
</template>
</control-section>
</div>
<div class="mwe-vue-pt-control-section__col3">
<control-section label="pagetriage-filter-predicted-issues-heading">
<template v-for="(_, issueName) in afcPossibleIssues" :key="issueName">
<input type="checkbox" :id="'mwe-vue-pt-filter-afc-predicted-issues-' + issueName" v-model="afcPossibleIssues[ issueName ]" />
<label :for="'mwe-vue-pt-filter-afc-predicted-issues-' + issueName">{{ $i18n( 'pagetriage-filter-predicted-issues-' + issueName ) }}</label> <br/>
</template>
</control-section>
</div>
</template>
<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>
</div>
</template>
</div>
</div>
<div class="mwe-vue-pt-control-buttons">
<wvui-button id="mwe-vue-pt-filter-set-button" class="mwe-vue-pt-button-green" action="progressive" type="primary" :disabled="!canSaveSettings">{{ $i18n( 'pagetriage-filter-set-button' ) }}</wvui-button>
</div>
</div>
</div>`
};
/**
* Interface for user to choose an article
*/
VueNPP.NPPFeedMenu = {
// wvui components are added separately
components: {
listItem: VueNPP.listItemComponent,
feedControlMenu: VueNPP.feedControlMenuComponent
},
data: function () {
return {
// Default example is [[FDP Hamburg]] for now
targetPageId: 70853005,
api: new mw.Api(),
apiError: false,
showListItem: false,
listItemProps: {}
};
},
methods: {
updatePageId: function ( newPageId ) {
this.targetPageId = newPageId;
},
updateEntry: function () {
console.log( this.targetPageId );
this.apiError = false;
this.showListItem = false;
this.listItemProps = {};
this.api.get( {
action: 'pagetriagelist',
page_id: this.targetPageId,
format: 'json',
formatversion: 2
} ).then(
( res ) => this.processEntry( res ),
( res ) => this.onApiFailure( res )
);
},
onApiFailure: function ( res ) {
console.log( res );
this.apiError = true;
},
processEntry: function ( res ) {
console.log( res );
if ( !res || !res.pagetriagelist || !res.pagetriagelist.pages
|| !res.pagetriagelist.pages[0]
) {
this.onApiFailure( res );
return;
}
const pageInfo = res.pagetriagelist.pages[0];
// Properties for <list-item>
const listItemProps = {};
listItemProps.position = 1;
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;
this.listItemProps = listItemProps;
this.showListItem = true;
}
},
template: `<div>
Feed controls:
<feed-control-menu></feed-control-menu>
<br>
Specific page entry, by page id: <wvui-input :value="targetPageId" v-on:input="updatePageId"></wvui-input>
<br>
<wvui-button action="progressive" type="primary" v-on:click="updateEntry">View entry</wvui-button>
<p v-show="apiError">Api error, see console</p>
<br>
<list-item v-show="showListItem" v-bind="listItemProps"></list-item>
</div>`
};
/**
* 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();
}
});
// </nowiki>