OOUI/Windows/Process Dialogs

From mediawiki.org

Process dialog windows encapsulate a process and all of the code necessary to complete it. If the process terminates with an error, a customizable error interface alerts users to the trouble, permitting the user to dismiss the error and try again when relevant. The ProcessDialog class is always extended and customized with the methods and static properties required for each process.

The process dialog box consists of a header that visually represents the ‘working’ state of long processes with an animation. The header contains the dialog title as well as two ActionWidgets:  a ‘safe’ action on the left (e.g., ‘Cancel’) and a ‘primary’ action on the right (e.g., ‘Done’). These actions are called ‘special’ actions. Any additional actions are called ‘other’ actions and are displayed in the dialog footer. Note that which actions are ‘special’ and which ‘other’ can be changed dynamically and/or accessed with the action set's getSpecial() or getOthers() methods.

Simple example[edit]

The following example illustrates a process dialog window with two ActionWidgets (‘Cancel’ and ‘Done’) displayed in its header. The dialog body contains a layout and message. Note that the window is not populated with this content when it is instantiated. Instead, its contents are added just before the window is opened for the first time, when the window manager calls the window's initialize() method. For an example of a window that is populated with contextual content passed at the time of opening, scroll down to the next example.

An example of a ProcessDialog

// Example: Creating and opening a process dialog window. 

// Subclass ProcessDialog.
function ProcessDialog( config ) {
	ProcessDialog.super.call( this, config );
}
OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
ProcessDialog.static.name = 'myDialog';
// Specify a static title and actions.
ProcessDialog.static.title = 'Process dialog';
ProcessDialog.static.actions = [
	{
		action: 'save',
		label: 'Done',
		flags: 'primary'
	},
	{
		label: 'Cancel',
		flags: 'safe'
	}
];

// Use the initialize() method to add content to the dialog's $body,
// to initialize widgets, and to set up event handlers.
ProcessDialog.prototype.initialize = function () {
	ProcessDialog.super.prototype.initialize.apply( this, arguments );

	this.content = new OO.ui.PanelLayout( {
		padded: true,
		expanded: false
	} );
	this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right. </p>' );

	this.$body.append( this.content.$element );
};

// Use the getActionProcess() method to specify a process to handle the
// actions (for the 'save' action, in this example).
ProcessDialog.prototype.getActionProcess = function ( action ) {
	var dialog = this;
	if ( action ) {
		return new OO.ui.Process( function () {
			dialog.close( {
				action: action
			} );
		} );
	}
// Fallback to parent handler.
	return ProcessDialog.super.prototype.getActionProcess.call( this, action );
};

// Get dialog height.
ProcessDialog.prototype.getBodyHeight = function () {
	return this.content.$element.outerHeight( true );
};

// Create and append the window manager.
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );

// Create a new dialog window.
var processDialog = new ProcessDialog( {
	size: 'small'
} );

// Add windows to window manager using the addWindows() method.
windowManager.addWindows( [ processDialog ] );

// Open the window.
windowManager.openWindow( processDialog );

The ‘primary’ and ‘safe’ actions (as well as any other actions specified) comprise an ActionSet. An action set can consist of multiple action widgets configured with the same action and/or the same flags. If multiple ‘safe’ and ‘primary’ actions are specified, the process dialog renders the first 'safe' and 'primary' action in its header. If an action is changed--to use a new flag, for example--the user interface is updated accordingly. To defer updating the interface until after all actions have been changed, use the forEach() method to iterate over the actions and update the user interface at the end of the iteration.

Using window data to set value of a text input when opening the dialog[edit]

The next example illustrates how the getSetupProcess() method is used to configure a window with contextual data that is passed at the time of opening.

An example of a ProcessDialog

// Example: Using getSetupProcess() to configure a window with data passed 
// at the time the window is opened. 

// Make a subclass of ProcessDialog 
function MyDialog( config ) {
	MyDialog.super.call( this, config );
}
OO.inheritClass( MyDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
MyDialog.static.name = 'myDialog';
// Specify the static configurations: title and action set
MyDialog.static.actions = [
	{ 
		flags: 'primary', 
		label: 'Continue', 
		action: 'open' 
	},
	{ 
		flags: 'safe', 
		label: 'Cancel' 
	 }
];

// Customize the initialize() function to add content and layouts: 
MyDialog.prototype.initialize = function () {
	MyDialog.super.prototype.initialize.call( this );
	this.panel = new OO.ui.PanelLayout( { 
		padded: true, 
		expanded: false 
	} );
	this.content = new OO.ui.FieldsetLayout();

	this.urlInput = new OO.ui.TextInputWidget();

	this.field = new OO.ui.FieldLayout( this.urlInput, { 
		label: 'Click continue to open the link in a new window', 
		align: 'top' 
	} );

	this.content.addItems( [ this.field ] );
	this.panel.$element.append( this.content.$element );
	this.$body.append( this.panel.$element );

	this.urlInput.connect( this, { 'change': 'onUrlInputChange' } );
};

// Specify any additional functionality required by the window (disable opening an empty URL, in this case)
MyDialog.prototype.onUrlInputChange = function ( value ) {
	this.actions.setAbilities( {
		open: !!value.length 
	} );
};

// Specify the dialog height (or don't to use the automatically generated height).
MyDialog.prototype.getBodyHeight = function () {
	// Note that "expanded: false" must be set in the panel's configuration for this to work.
	// When working with a stack layout, you can use:
	//   return this.panels.getCurrentItem().$element.outerHeight( true );
	return this.panel.$element.outerHeight( true );
};

// Use getSetupProcess() to set up the window with data passed to it at the time 
// of opening (e.g., url: 'http://www.mediawiki.org', in this example). 
MyDialog.prototype.getSetupProcess = function ( data ) {
	data = data || {};
	return MyDialog.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
		// Set up contents based on data
		this.urlInput.setValue( data.url );
	}, this );
};

// Specify processes to handle the actions.
MyDialog.prototype.getActionProcess = function ( action ) {
	if ( action === 'open' ) {
		// Create a new process to handle the action
		return new OO.ui.Process( function () {
			window.open( this.urlInput.getValue());
		}, this );
	}
	// Fallback to parent handler
	return MyDialog.super.prototype.getActionProcess.call( this, action );
};

// Use the getTeardownProcess() method to perform actions whenever the dialog is closed. 
// This method provides access to data passed into the window's close() method 
// or the window manager's closeWindow() method.
MyDialog.prototype.getTeardownProcess = function ( data ) {
	return MyDialog.super.prototype.getTeardownProcess.call( this, data )
	.first( function () {
	// Perform any cleanup as needed
	}, this );
};

// Create and append a window manager.
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );

// Create a new process dialog window.
var myDialog = new MyDialog();

// Add the window to window manager using the addWindows() method.
windowManager.addWindows( [ myDialog ] );

// Open the window!   
windowManager.openWindow( myDialog, { url: 'https://www.mediawiki.org' } );

For a full list of supported methods and configuration options for process dialogs, please see the code-level documentation.

Action sets[edit]

ActionSets manage the behavior of the actions that comprise them, allowing developers to control which actions are available for specific contexts (modes) and circumstances (abilities):

  • modes: When an action widget is configured with modes (e.g., ‘edit’ or ‘preview’), the action set can make the action available or not based on those modes (set with the setMode() method). Each action will only be available if it is configured for the current mode. (See below example.)
  • abilities: When an action widget is configured with an action, the action set can make that action available or not based on abilities (set with the setAbilities() method). If a ‘save’ action is used to save content, for example, abilities can be used to disable the action until the content is determined to be valid. If multiple action widgets execute the ‘save’ action, all will be disabled until the criteria are met.
  • flags: primary and safe flags control the layout of the options. back and close flags provide defaults icons. See OOUI/Elements/Flagged#ProcessDialog.

The following example illustrates an action set that contains four action widgets: a primary action (‘continue’), two safe actions (‘cancel’ and ‘back’), and a ‘help’ action. Several of the actions have also been configured with modes: ‘edit’ or ‘help.’

// Example: an action set that contains four action widgets.
ProcessDialog.static.actions = [
	{ 
		action: 'continue', 
		modes: 'edit', 
		label: 'Continue', 
		flags: [ 'primary', 'progressive' ] 
	},
	{ 
		action: 'help', 
		modes: 'edit', 
		label: 'Help' 
	},
	{ 
		modes: 'edit', 
		label: 'Cancel', 
		flags: [ 'safe', 'close' ]
	},
	{ 
		action: 'back', 
		modes: 'help', 
		label: 'Back', 
		flags: [ 'safe', 'back' ]
	} 
];

The process dialog in the next example uses the above action set to customize available actions based on the modes.

Example: Using modes and action sets.

Example: Using modes and action sets.

// Example: A process dialog that uses an action set with modes.

// Subclass ProcessDialog.
function ProcessDialog( config ) {
	ProcessDialog.super.call( this, config );
}
OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
ProcessDialog.static.name = 'myDialog';
// Specify a title and an action set that uses modes ('edit' and 'help' mode, in this example).
ProcessDialog.static.title = 'Process dialog';
ProcessDialog.static.actions = [
	{ 
		action: 'continue', 
		modes: 'edit', 
		label: 'Continue', 
		flags: [ 'primary', 'progressive' ] 
	},
	{ 
		action: 'help', 
		modes: 'edit', 
		label: 'Help' 
	},
	{ 
		modes: 'edit', 
		label: 'Cancel', 
		flags: [ 'safe', 'close' ]
	},
	{ 
		action: 'back', 
		modes: 'help', 
		label: 'Back', 
		flags: [ 'safe', 'back' ]
	}
];

// Customize the initialize() method to add content and set up event handlers. 
// This example uses a stack layout with two panels: one displayed for 
// edit mode and one for help mode.
ProcessDialog.prototype.initialize = function () {
	ProcessDialog.super.prototype.initialize.apply( this, arguments );
	
	this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.panel1.$element.append( '<p>This dialog uses an action set configured with modes. This is edit mode. Click \'help\' to see help mode. </p>' );
	this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.panel2.$element.append( '<p>This is help mode. Only the \'back\' button is configured to be visible here. Click \'back\' to return to \'edit\' mode</p>' );
	this.stackLayout= new OO.ui.StackLayout( {
		items: [ this.panel1, this.panel2 ]
	} ); 
	this.$body.append( this.stackLayout.$element );    
};

// Set up the initial mode of the window ('edit', in this example.)  
ProcessDialog.prototype.getSetupProcess = function ( data ) {
	return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
		this.actions.setMode( 'edit' );
	}, this );
};

// Use the getActionProcess() method to set the modes and displayed item.
ProcessDialog.prototype.getActionProcess = function ( action ) {
		
	if ( action === 'help' ) {
		// Set the mode to help.
		this.actions.setMode( 'help' );
		// Show the help panel.
		this.stackLayout.setItem( this.panel2 );
	} else if ( action === 'back' ) {
		// Set the mode to edit.
		this.actions.setMode( 'edit' );
		// Show the edit panel.
		this.stackLayout.setItem( this.panel1 );
	} else if ( action === 'continue' ) {
		var dialog = this;   
		return new OO.ui.Process( function () {
			// Do something about the edit.
			dialog.close();
		} );
	}
	return ProcessDialog.super.prototype.getActionProcess.call( this, action );
};

// Get dialog height.
ProcessDialog.prototype.getBodyHeight = function () {
	return this.panel1.$element.outerHeight( true );
};

// Create and append the window manager.
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );

// Create a new dialog window.
var processDialog = new ProcessDialog( {
	size: 'medium'
} );

// Add windows to window manager using the addWindows() method.
windowManager.addWindows( [ processDialog ] );

// Open the window.
windowManager.openWindow( processDialog );

For a full list of supported methods and configuration options for action sets, please see the code-level documentation.

Processes and errors[edit]

Process dialogs execute a process, which is a list of ‘steps’ that are called in sequence. If the process fails, an error is generated. Depending on how the error is configured, users can dismiss the error and try the process again, or not.

Each ‘step’ of a process can be a number, a jQuery promise, or a function:

  • function: If the step is a function, the process will execute it. Optionally, a function context can be specified as well. The process will stop if the function returns a Boolean ‘false’.
  • promise: If the step is a  jQuery promise, the process will use the promise to either continue to the next step when the promise is resolved or to stop when the promise is rejected.
  • number: If the step is a number, the process will wait for the specified number of milliseconds before proceeding.

A ‘step’ can be added to the beginning of a process with the first() method, or to the end of it with the next() method.

The process is started with the execute() method, which returns a promise that is either resolved when all steps have completed or rejected with an error message if any of the steps return a Boolean ‘false’ or a promise is rejected. If a process is stopped, its remaining steps will not be performed.

The error messages used by the process dialog are configured with a required string argument (or jQuery selection) that is used as the error message as well as optional settings (recoverable or warning) that influence the appearance and functionality of the error interface.

The basic error message contains a formatted error message as well as two buttons: ‘Dismiss’ and ‘Try again’ (i.e., the error is recoverable by default). If recoverable is set to false, the ‘Try again’ button will not be rendered and the widget that initiated the failed process will be disabled.

Errors can also be configured as warnings. If warning is set to true, the error interface will include a ‘Dismiss’ and a ‘Continue’ button, which will try the process again. A warning might be used before a destructive action, such as ‘Delete all files’ to confirm that this is the user’s intent. It is the responsibility of the developer to ensure that the warning is not triggered a second time if the user chooses to continue.

Note that the appearance of the error message can be customized by using a jQuery selection instead of a string for the value of the required message argument. A jQuery selection will allow you to add links and/or spans for styling select parts of the error message. The error title is not configurable.

The following is an example of a process dialog that has been configured to display a recoverable error message (for the ‘Save’ process) and a non-recoverable error message (for the ‘Delete’ process). The example also uses a time delay to demonstrate how the window visually represents a long-running process with a header animation.

Example: Processes and errors.

// Example: a process dialog with customized error interfaces.
function BrokenDialog( config ) {
	BrokenDialog.super.call( this, config );
	this.broken = false;
}
OO.inheritClass( BrokenDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
BrokenDialog.static.name = 'brokenDialog';
// Specify static actions and title
BrokenDialog.static.title = 'Broken dialog';
BrokenDialog.static.actions = [
	{ action: 'save', label: 'Save', flags: [ 'primary', 'constructive' ] },
	{ action: 'delete', label: 'Delete', flags: 'destructive' },
	{ action: 'cancel', label: 'Cancel', flags: 'safe' }
];

// Get the height.
BrokenDialog.prototype.getBodyHeight = function () {
	return 250;
};
	
// Add content to the dialog body and setup event handlers.
BrokenDialog.prototype.initialize = function () {
	BrokenDialog.super.prototype.initialize.apply( this, arguments );
	this.content = new OO.ui.PanelLayout( { padded: true } );
	this.fieldset = new OO.ui.FieldsetLayout( {
		label: 'Dialog with error handling', icon: 'alert'
	} );
	this.description = new OO.ui.LabelWidget( {
		label: 'Deleting will fail and will not be recoverable. ' +
				'Saving will fail the first time, but succeed the second time.'
	} );
	this.fieldset.addItems( [ this.description ] );
	this.content.$element.append( this.fieldset.$element );
	this.$body.append( this.content.$element );
};

// Add a 'broken' function to getSetupProcess() for purposes of this example.
BrokenDialog.prototype.getSetupProcess = function ( data ) {
	return BrokenDialog.super.prototype.getSetupProcess.call( this, data )
		.next( function () {
			this.broken = true;
		}, this );
};
 
BrokenDialog.prototype.getActionProcess = function ( action ) {
	return BrokenDialog.super.prototype.getActionProcess.call( this, action )
		.next( function () {
			return 1000;
		}, this )
		.next( function () {
			var closing;

			if ( action === 'save' ) {
				if ( this.broken ) {
					this.broken = false;
					return new OO.ui.Error( 'Server did not respond' );
				}
			} else if ( action === 'delete' ) {
				return new OO.ui.Error( 'Permission denied', { recoverable: false } );
			}
			closing = this.close( { action: action } );
			if ( action === 'save' ) {
				// Return a promise to remain pending while closing
				return closing;
			}
			return BrokenDialog.super.prototype.getActionProcess.call( this, action );
		}, this );
};

// Create and append the window manager.
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );

// Make the window. 
var brokenDialog = new BrokenDialog( {
		size: 'medium'
	} );

// Add windows to the window manager using the window manager’s addWindows() method.
windowManager.addWindows( [ brokenDialog ] );
windowManager.openWindow( brokenDialog );

For a full list of supported methods and configuration options, please see the code-generated documentation for processes and errors.