MediaWiki:Gadget-cws-manager-dialog.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * @class
 * @property {CwsManager} cwsManager
 * @property {mw.Title} proposalTitle
 * @property {boolean} watched
 * @property {string|null} action
 */
class Dialog extends OO.ui.ProcessDialog {

	static name = 'cwsManagerDialog';
	static size = 'medium';
	static title = 'Manage proposal';
	static actions = [
		{
			action: 'submit',
			label: 'Submit',
			flags: [ 'primary', 'progressive' ]
		},
		{
			label: 'Cancel',
			flags: [ 'safe', 'close' ]
		}
	];

	/**
	 * @param {CwsManager} cwsManagerContext
	 * @param {string} proposalPageName
	 * @param {boolean} watched
	 * @constructor
	 */
	constructor( cwsManagerContext, proposalPageName, watched ) {
		super();
		this.cwsManager = cwsManagerContext;
		this.proposalTitle = mw.Title.newFromText( proposalPageName ).getPrefixedText();
		this.watched = watched;
		this.action = null;

		// Add properties needed by the ES5-based OOUI inheritance mechanism.
		// This roughly simulates OO.inheritClass()
		Dialog.parent = Dialog.super = OO.ui.ProcessDialog;
		OO.initClass( OO.ui.ProcessDialog );
		Dialog.static = Object.create( OO.ui.ProcessDialog.static );
		Object.keys( Dialog ).forEach( ( key ) => {
			Dialog.static[ key ] = Dialog[ key ];
		} );
	}

	/**
	 * @param {...*} args
	 * @override
	 */
	initialize( ...args ) {
		super.initialize( ...args );
		this.actionFieldset = this.#getActionFieldset();
		this.moveFieldset = this.#getMoveFieldset();
		this.moveFieldset.toggle( false );
		this.archiveFieldset = this.#getArchiveFieldset();
		this.archiveFieldset.toggle( false );
		this.unarchiveFieldset = this.#getUnarchiveFieldset();
		this.unarchiveFieldset.toggle( false );
		this.approveFieldset = this.#getApproveFieldset();
		this.approveFieldset.toggle( false );
		this.watchFieldset = this.#getWatchFieldset();
		this.formPanel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
		this.formPanel.$element.append(
			this.actionFieldset.$element,
			this.moveFieldset.$element,
			this.archiveFieldset.$element,
			this.unarchiveFieldset.$element,
			this.approveFieldset.$element,
			this.watchFieldset.$element
		);
		this.$body.append( this.formPanel.$element );
	}

	/**
	 * Update the view based on which action was selected.
	 *
	 * @param {OO.ui.RadioOptionWidget} radioButton
	 */
	onActionInputChange( radioButton ) {
		this.action = radioButton.data;
		this.moveFieldset.toggle( this.action === 'move' );
		this.approveFieldset.toggle( this.action === 'approve' );
		this.archiveFieldset.toggle( this.action === 'archive' );
		this.unarchiveFieldset.toggle( this.action === 'unarchive' );
		this.getActions().setAbilities( { submit: true } );
		this.updateSize();
	}

	/**
	 * Show/hide the archive reason input, as applicable.
	 *
	 * @param {OO.ui.MenuOptionWidget} option
	 */
	onArchiveReasonInputChange( option ) {
		const wikitext = option.data.reason;
		if ( wikitext.includes( '$1' ) ) {
			this.archivePromptFieldLayout.setLabel( option.data.prompt );
			this.archivePromptFieldLayout.toggle( true );
		} else {
			this.archivePromptFieldLayout.toggle( false );
		}
		if ( option.data.note ) {
			this.archiveReasonFieldLayout.setNotices( [
				option.data.note
			] );
		} else {
			this.archiveReasonFieldLayout.setNotices( [] );
		}
		this.updateSize();
	}

	/**
	 * Get the fieldset for selecting an action on the proposal.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getActionFieldset() {
		const [ categoryName ] = this.#getTitleParts();
		const moveRadio = new OO.ui.RadioOptionWidget( {
			label: 'Rename proposal or move to a different category',
			data: 'move'
		} );
		const archiveRadio = new OO.ui.RadioOptionWidget( {
			label: 'Move to the archive',
			data: 'archive'
		} );
		archiveRadio.toggle( categoryName !== 'Archive' );
		const unarchiveRadio = new OO.ui.RadioOptionWidget( {
			label: 'Unarchive proposal',
			data: 'unarchive'
		} );
		unarchiveRadio.toggle( categoryName === 'Archive' );
		const approveRadio = new OO.ui.RadioOptionWidget( {
			label: 'Approve and prepare for translation',
			data: 'approve'
		} );
		approveRadio.toggle(
			![ 'Archive', 'Larger suggestions', 'Untranslated' ].includes( categoryName )
		);

		const radioSelect = new OO.ui.RadioSelectWidget( {
			items: [ moveRadio, archiveRadio, unarchiveRadio, approveRadio ]
		} );
		radioSelect.connect( this, {
			choose: this.onActionInputChange
		} );

		return new OO.ui.FieldsetLayout( {
			label: 'Select action to take on this proposal',
			items: [ radioSelect ]
		} );
	}

	/**
	 * Get the fieldset for moving a proposal.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getMoveFieldset() {
		const [ categoryName, proposalName ] = this.#getTitleParts();
		const moveFieldset = new OO.ui.FieldsetLayout( { label: 'Move proposal' } );
		this.moveTitleInput = new OO.ui.TextInputWidget( {
			value: proposalName,
			indicator: 'required'
		} );
		this.moveCategoryInput = this.#getCategoryInput( categoryName );
		this.moveReasonInput = new OO.ui.TextInputWidget();

		moveFieldset.addItems( [
			new OO.ui.FieldLayout( this.moveTitleInput, {
				label: 'Proposal title:',
				align: 'top'
			} ),
			new OO.ui.FieldLayout( this.moveCategoryInput, {
				label: 'Category:',
				align: 'top'
			} ),
			new OO.ui.FieldLayout( this.moveReasonInput, {
				label: 'Optional comment for the logs:',
				align: 'top'
			} )
		] );

		return moveFieldset;
	}

	/**
	 * Get the fieldset for unarchiving a proposal.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getUnarchiveFieldset() {
		const [ categoryName, proposalName ] = this.#getTitleParts();
		const unarchiveFieldset = new OO.ui.FieldsetLayout( { label: 'Unarchive proposal' } );
		this.unarchiveTitleInput = new OO.ui.TextInputWidget( {
			value: proposalName,
			indicator: 'required'
		} );
		this.unarchiveCategoryInput = this.#getCategoryInput( categoryName );
		this.unarchiveReasonInput = new OO.ui.TextInputWidget();

		unarchiveFieldset.addItems( [
			new OO.ui.Element( {
				text: 'Unarchiving will remove the archive rationale, if present, ' +
					'and move the proposal to the specified category.'
			} ),
			new OO.ui.FieldLayout( this.unarchiveTitleInput, {
				label: 'Proposal title:',
				align: 'top'
			} ),
			new OO.ui.FieldLayout( this.unarchiveCategoryInput, {
				label: 'Category:',
				align: 'top'
			} ),
			new OO.ui.FieldLayout( this.unarchiveReasonInput, {
				label: 'Optional comment for the logs:',
				align: 'top'
			} )
		] );

		return unarchiveFieldset;
	}

	/**
	 * Get a new category input.
	 *
	 * @param {string} categoryName
	 * @return {OO.ui.DropdownInputWidget}
	 * @private
	 */
	#getCategoryInput( categoryName ) {
		return new OO.ui.DropdownInputWidget( {
			options: this.cwsManager.config.categories.concat( [ 'Larger suggestions' ] ).map( ( category ) => {
				return { data: category, label: category };
			} ),
			value: categoryName
		} );
	}

	/**
	 * Get the fieldset for archiving a proposal.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getArchiveFieldset() {
		const reasons = this.cwsManager.config.archive_reasons;
		const dropdownOptions = [];
		reasons.forEach( ( reason ) => {
			dropdownOptions.push( new OO.ui.MenuOptionWidget( {
				data: reason,
				label: reason.display || reason.reason
			} ) );
		} );

		const archiveFieldset = new OO.ui.FieldsetLayout( {
			label: 'Archive proposal'
		} );

		this.archiveReasonInput = new OO.ui.DropdownWidget( {
			label: 'Select a rationale for archiving…',
			menu: {
				items: dropdownOptions
			}
		} );
		this.archiveReasonInput.getMenu().on( 'select', this.onArchiveReasonInputChange.bind( this ) );
		this.archiveReasonFieldLayout = new OO.ui.FieldLayout( this.archiveReasonInput, {
			label: 'Rationale:',
			align: 'top'
		} );

		this.archivePromptInput = new OO.ui.TextInputWidget( {
			indicator: 'required'
		} );
		this.archivePromptFieldLayout = new OO.ui.FieldLayout( this.archivePromptInput, {
			align: 'top'
		} );
		this.archivePromptFieldLayout.toggle( false );

		this.archiveTextarea = new OO.ui.MultilineTextInputWidget( {
			placeholder: 'Add any additional messaging here, which will be posted as a comment in the Discussion section.'
		} );

		const formElements = [
			this.archiveReasonFieldLayout,
			this.archivePromptFieldLayout,
			new OO.ui.FieldLayout( this.archiveTextarea, {
				label: 'Optional additional comment (encouraged):',
				align: 'top'
			} )
		];

		archiveFieldset.addItems( formElements );
		return archiveFieldset;
	}

	/**
	 * Get the fieldset for approving a proposal.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getApproveFieldset() {
		const content = new OO.ui.HtmlSnippet(
			'<p>This action will move the proposal to a new /Proposal subpage, ' +
			'convert it to use the proposal template, and insert translate tags. ' +
			'Afterwards, the &lt;translate&gt; tags will become visible in transclusions until ' +
			'the page is marked for translation, so <strong>please act quickly</strong>. ' +
			'Please first verify no use of &lt;tvar&gt; or other fixes to the translation page are needed.'
		);
		return new OO.ui.FieldsetLayout( {
			label: 'Approve proposal',
			content: [ content ]
		} );
	}

	/**
	 * Get the fieldset for the bottom options, such as to watch the proposal page.
	 *
	 * @return {OO.ui.FieldsetLayout}
	 * @private
	 */
	#getWatchFieldset() {
		const watchFieldset = new OO.ui.FieldsetLayout();
		this.watchCheckbox = new OO.ui.CheckboxInputWidget( { selected: this.watched } );
		const formElements = [
			new OO.ui.FieldLayout( this.watchCheckbox, {
				label: 'Watch proposal page',
				align: 'inline'
			} )
		];
		watchFieldset.addItems( formElements );
		return watchFieldset;
	}

	/**
	 * Get the proposal category and subpage name.
	 *
	 * @return {Array<string>} [ category name, proposal name ]
	 * @private
	 */
	#getTitleParts() {
		const categoryName = this.proposalTitle.split( '/' )[ 1 ];
		const titleRegex = new RegExp(
			`^${this.cwsManager.config.survey_root}/${categoryName}/`
		);
		return [ categoryName, this.proposalTitle.replace( titleRegex, '' ) ];
	}

	/**
	 * @param {Object} data
	 * @return {OO.ui.Process}
	 * @override
	 */
	getSetupProcess( data ) {
		return super.getSetupProcess( data )
			.next( () => {
				this.getActions().setAbilities( { submit: !!this.action } );
			} );
	}

	/**
	 * @param {string} action
	 * @return {OO.ui.Process}
	 * @override
	 */
	getActionProcess( action ) {
		return super.getActionProcess( action )
			.next( () => {
				if ( action === 'submit' ) {
					return this.cwsManager.submit(
						this.action,
						this.getFormData( this.action )
					);
				}

				return super.getActionProcess( action );
			} )
			.next( () => {
				if ( action === 'submit' ) {
					// Redirect to the proposal (which may be the same page).
					// In the case of moving proposals, we rely on redirects.
					window.location.replace( mw.util.getUrl( this.proposalTitle ) );
				}

				return super.getActionProcess( action );
			} );
	}

	/**
	 * Get an Object containing all the data we need for submission.
	 *
	 * @param {string} action
	 * @return {Object}
	 */
	getFormData( action ) {
		const data = {
			proposalTitle: this.proposalTitle,
			watchProposal: this.watchCheckbox.isSelected()
		};
		switch ( action ) {
			case 'move':
				return Object.assign( data, {
					newName: this.moveTitleInput.getValue(),
					newCategory: this.moveCategoryInput.getValue(),
					reason: this.moveReasonInput.getValue()
				} );
			case 'archive':
				return Object.assign( data, {
					reason: this.archiveReasonInput.getMenu().findSelectedItem().data.reason,
					param: this.archivePromptInput.getValue(),
					comment: this.archiveTextarea.getValue(),
					// 'archive' also 'move' so we need the subpage title as well
					newName: this.#getTitleParts()[ 1 ]
				} );
			case 'unarchive':
				return Object.assign( data, {
					newName: this.unarchiveTitleInput.getValue(),
					newCategory: this.unarchiveCategoryInput.getValue(),
					reason: this.unarchiveReasonInput.getValue()
				} );
			case 'approve':
				// Falls through, no extra data needed.
		}
		return data;
	}
}

module.exports = Dialog;