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-vue-pt-control-section">
<span class="mwe-vue-pt-control-label">
<b>{{ $i18n( label ) }}</b>
</span>
<div class="mwe-vue-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-vue-pt-filter-' + type + '-date-range-from'">{{ $i18n( 'pagetriage-filter-date-range-from' ) }}</label>
<input type="date" :value="fromModel" @input="updateFrom" :id="'mwe-vue-pt-filter-' + type + '-date-range-from'" :placeholder="$i18n( 'pagetriage-filter-date-range-format-placeholder' )" /> <br/>
<label :for="'mwe-vue-pt-filter-' + type + '-date-range-to'">{{ $i18n( 'pagetriage-filter-date-range-to' ) }}</label>
<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.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 <input>
props: {
inputId: { type: String, required: true },
inputModel: { type: String, 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: `<template>
<input :type="type" :id="inputId" :value="value" :checked="isChecked" @change="onChange" />
<label :for="inputId">{{ $i18n( labelMsg ) }}</label> <br v-if="haveBreak" />
</template>`
};
/**
* 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' },
// 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
},
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 ) {
// Shortcuts
const ref = VueCompositionAPI.ref;
const computed = VueCompositionAPI.computed;
const currentView = ref( props.startOptions.currentView );
//#region NPP
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( '' );
const nppDateTo = ref( '' );
//#endregion
//#region AFC
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 );
// 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() );
} );
//#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">
<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" />
</p>
<span class="mwe-vue-pt-control-label">
<b>{{ $i18n( 'pagetriage-showing' ) }} </b>
{{ currentlyShowingText }}
</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">
<labeled-input type="radio" input-id="mwe-vue-pt-sort-newest" v-model:inputModel="nppSortDir" label-msg="pagetriage-newest" value="newestfirst" :no-break="true" />
<labeled-input type="radio" input-id="mwe-vue-pt-sort-oldest" v-model:inputModel="nppSortDir" label-msg="pagetriage-oldest" value="oldestfirst" :no-break="true" />
</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">
<labeled-input type="checkbox" input-id="mwe-pt-filter-unreviewed-edits" v-model:inputModel="nppIncludeUnreviewed" label-msg="pagetriage-filter-unreviewed-edits" />
<labeled-input type="checkbox" input-id="mwe-pt-filter-reviewed-edits" v-model:inputModel="nppIncludeReviewed" label-msg="pagetriage-filter-reviewed-edits" />
</control-section>
<control-section label="pagetriage-filter-type-show-heading">
<labeled-input type="checkbox" input-id="mwe-vue-pt-filter-nominated-for-deletion" v-model:inputModel="nppIncludeNominated" label-msg="pagetriage-filter-nominated-for-deletion" />
<labeled-input type="checkbox" input-id="mwe-vue-pt-filter-redirects" v-model:inputModel="nppIncludeRedirects" label-msg="pagetriage-filter-redirects" />
<labeled-input type="checkbox" input-id="mwe-vue-pt-filter-others" v-model:inputModel="nppIncludeOthers" label-msg="pagetriage-filter-others" />
</control-section>
</div>
<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" :input-id="'mwe-vue-pt-filter-npp-predicted-class-' + ratingName" v-model:inputModel="nppPredictedRating[ ratingName ]" :label-msg="'pagetriage-filter-predicted-class-' + ratingName" />
</control-section>
</div>
<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" :input-id="'mwe-vue-pt-filter-npp-predicted-issues-' + issueName" 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>
</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">
<labeled-input v-for="filter in nppFilters" :key="filter" type="radio" name="mwe-vue-pt-filter-radio" :input-id="'mwe-vue-pt-filter-' + filter" :value="filter" v-model:inputModel="nppFilter" :label-msg="'pagetriage-filter-' + filter" />
<labeled-input type="radio" name="mwe-vue-pt-filter-radio" input-id="mwe-vue-pt-filter-user-selected" 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"/> <br/>
<labeled-input type="radio" name="mwe-vue-pt-filter-radio" input-id="mwe-vue-pt-filter-all" v-model:inputModel="nppFilter" label-msg="pagetriage-filter-all" value="all" />
</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">
<labeled-input v-for="state in afcSubmissionStates" :key="state" type="radio" name="mwe-vue-pt-filter-afc-radio" :input-id="'mwe-vue-pt-filter-afc-' + state" :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>
</div>
<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" :input-id="'mwe-vue-pt-filter-afc-predicted-class-' + ratingName" v-model:inputModel="afcPredictedRating[ ratingName ]" :label-msg="'pagetriage-filter-predicted-class-' + ratingName" />
</control-section>
</div>
<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" :input-id="'mwe-vue-pt-filter-afc-predicted-issues-' + issueName" v-model:inputModel="afcPossibleIssues[ issueName ]" :label-msg="'pagetriage-filter-predicted-issues-' + issueName" />
</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>