Index: src/wp-admin/includes/user.php
===================================================================
--- src/wp-admin/includes/user.php	(revision 42985)
+++ src/wp-admin/includes/user.php	(working copy)
@@ -709,23 +709,23 @@
  * @access private
  */
 function _wp_personal_data_handle_actions() {
-	if ( isset( $_POST['export_personal_data_email_retry'] ) ) { // WPCS: input var ok.
+	if ( isset( $_POST['privacy_action_email_retry'] ) ) { // WPCS: input var ok.
 		check_admin_referer( 'bulk-privacy_requests' );
 
-		$request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_retry'] ) ) ) ); // WPCS: input var ok, sanitization ok.
+		$request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) ); // WPCS: input var ok, sanitization ok.
 		$result     = _wp_privacy_resend_request( $request_id );
 
 		if ( is_wp_error( $result ) ) {
 			add_settings_error(
-				'export_personal_data_email_retry',
-				'export_personal_data_email_retry',
+				'privacy_action_email_retry',
+				'privacy_action_email_retry',
 				$result->get_error_message(),
 				'error'
 			);
 		} else {
 			add_settings_error(
-				'export_personal_data_email_retry',
-				'export_personal_data_email_retry',
+				'privacy_action_email_retry',
+				'privacy_action_email_retry',
 				__( 'Confirmation request re-resent successfully.' ),
 				'updated'
 			);
@@ -902,18 +902,23 @@
  * @access private
  */
 function _wp_personal_data_removal_page() {
-	if ( ! current_user_can( 'manage_options' ) ) {
+	if ( ! current_user_can( 'delete_users' ) ) {
 		wp_die( esc_html__( 'Sorry, you are not allowed to manage privacy on this site.' ) );
 	}
 
 	_wp_personal_data_handle_actions();
 
+	// "Borrow" xfn.js for now so we don't have to create new files.
+	wp_enqueue_script( 'xfn' );
+
 	$requests_table = new WP_Privacy_Data_Removal_Requests_Table( array(
 		'plural'   => 'privacy_requests',
 		'singular' => 'privacy_request',
 	) );
+
 	$requests_table->process_bulk_action();
 	$requests_table->prepare_items();
+
 	?>
 	<div class="wrap nosubsub">
 		<h1><?php esc_html_e( 'Remove Personal Data' ); ?></h1>
@@ -1358,8 +1363,18 @@
 	 * @return string
 	 */
 	public function column_email( $item ) {
+		$exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
+		$exporters_count = count( $exporters );
+		$request_id      = $item['request_id'];
+		$nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
+
+		$download_data_markup = '<div class="download_personal_data" data-exporters-count="' . esc_attr( $exporters_count ) . '" data-request-id="' . esc_attr( $request_id ) . '" data-nonce="' . esc_attr( $nonce ) . '">' .
+			'<span class="download_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
+			'<span style="display:none" class="download_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
+			'<span style="display:none" class="download_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
+
 		$row_actions = array(
-			'download_data' => __( 'Download Personal Data' ),
+			'download_data' => $download_data_markup,
 		);
 
 		return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( $row_actions ) );
@@ -1383,7 +1398,7 @@
 				// TODO Complete in follow on patch.
 				break;
 			case 'request-failed':
-				submit_button( __( 'Retry' ), 'secondary', 'export_personal_data_email_retry[' . $item['request_id'] . ']', false );
+				submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item['request_id'] . ']', false );
 				break;
 			case 'request-completed':
 				echo '<a href="' . esc_url( wp_nonce_url( add_query_arg( array(
@@ -1428,15 +1443,24 @@
 	 * @return string
 	 */
 	public function column_email( $item ) {
-		$row_actions = array(
-			// TODO Complete in follow on patch.
-			'remove_data' => __( 'Remove Personal Data' ),
-		);
+		$row_actions = array();
 
-		// If we have a user ID, include a delete user action.
-		if ( ! empty( $item['user_id'] ) ) {
-			// TODO Complete in follow on patch.
-			$row_actions['delete_user'] = __( 'Delete User' );
+		// Allow the administrator to "force remove" the personal data even if confirmation has not yet been received
+		$status = get_post_status( $item['request_id'] );
+		if ( 'request-confirmed' !== $status ) {
+			$erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
+			$erasers_count = count( $erasers );
+			$request_id    = $item['request_id'];
+			$nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
+
+			$remove_data_markup = '<div class="remove_personal_data force_remove_personal_data" data-erasers-count="' . esc_attr( $erasers_count ) . '" data-request-id="' . esc_attr( $request_id ) . '" data-nonce="' . esc_attr( $nonce ) . '">' .
+				'<span class="remove_personal_data_idle"><a href="#" >' . __( 'Force Remove Personal Data' ) . '</a></span>' .
+				'<span style="display:none" class="remove_personal_data_processing" >' . __( 'Removing Data...' ) . '</span>' .
+				'<span style="display:none" class="remove_personal_data_failed">' . __( 'Force Remove Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
+
+			$row_actions = array(
+				'remove_data' => $remove_data_markup,
+			);
 		}
 
 		return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( $row_actions ) );
@@ -1450,6 +1474,35 @@
 	 * @param array $item Item being shown.
 	 */
 	public function column_next_steps( $item ) {
+		$status = get_post_status( $item['request_id'] );
+
+		switch ( $status ) {
+			case 'request-pending':
+				esc_html_e( 'Waiting for confirmation' );
+				break;
+			case 'request-confirmed':
+				$erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
+				$erasers_count = count( $erasers );
+				$request_id    = $item['request_id'];
+				$nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
+
+				$remove_data_markup = '<div class="remove_personal_data" data-force-erase="1" data-erasers-count="' . esc_attr( $erasers_count ) . '" data-request-id="' . esc_attr( $request_id ) . '" data-nonce="' . esc_attr( $nonce ) . '">' .
+					'<span class="remove_personal_data_idle"><a class="button" href="#" >' . __( 'Remove Personal Data' ) . '</a></span>' .
+					'<span style="display:none" class="remove_personal_data_processing button updating-message" >' . __( 'Removing Data...' ) . '</span>' .
+					'<span style="display:none" class="remove_personal_data_failed">' . __( 'Removing Data Failed!' ) . ' <a class="button" href="#" >' . __( 'Retry' ) . '</a></span>';
+
+				echo $remove_data_markup;
+				break;
+			case 'request-failed':
+				submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item['request_id'] . ']', false );
+				break;
+			case 'request-completed':
+				echo '<a href="' . esc_url( wp_nonce_url( add_query_arg( array(
+					'action' => 'delete',
+					'request_id' => array( $item['request_id'] )
+				), admin_url( 'tools.php?page=remove_personal_data' ) ), 'bulk-privacy_requests' ) ) . '">' . esc_html__( 'Remove request' ) . '</a>';
+				break;
+		}
 	}
 
 }
Index: src/wp-admin/js/xfn.js
===================================================================
--- src/wp-admin/js/xfn.js	(revision 42985)
+++ src/wp-admin/js/xfn.js	(working copy)
@@ -20,3 +20,124 @@
 		$( '#link_rel' ).val( ( isMe ) ? 'me' : inputs.substr( 0,inputs.length - 1 ) );
 	});
 });
+
+// Privacy request action handling
+
+jQuery( document ).ready( function( $ ) {
+	var strings = window.privacyToolsL10n || {};
+
+	function set_action_state( $action, state ) {
+		$action.children().hide();
+		$action.children( '.' + state ).show();
+	}
+
+	function clearResultsAfterRow( $requestRow ) {
+		if ( $requestRow.next().hasClass( 'request-results' ) ) {
+			$requestRow.next().remove();
+		}
+	}
+
+	function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
+		clearResultsAfterRow( $requestRow );
+		if ( additionalMessages.length ) {
+			// TODO - render additionalMessages after the summaryMessage
+		}
+
+		$requestRow.after( function() {
+			return '<tr class="request-results"><td colspan="5"><div class="notice inline notice-alt ' + classes + '"><p>' +
+				summaryMessage +
+				'</p></div></td></tr>';
+		} );
+	}
+
+	$( '.remove_personal_data a' ).click( function( event ) {
+		event.preventDefault();
+		event.stopPropagation();
+
+		var $this         = $( this );
+		var $action       = $this.parents( '.remove_personal_data' );
+		var $requestRow   = $this.parents( 'tr' );
+		var requestID     = $action.data( 'request-id' );
+		var nonce         = $action.data( 'nonce' );
+		var erasersCount  = $action.data( 'erasers-count' );
+
+		var removedCount  = 0;
+		var retainedCount = 0;
+		var messages      = [];
+
+		$action.blur();
+		clearResultsAfterRow( $requestRow );
+
+		function on_erasure_done_success() {
+			set_action_state( $action, 'remove_personal_data_idle' );
+			var summaryMessage = strings.noDataFound;
+			var classes = 'notice-success';
+			if ( 0 == removedCount ) {
+				if ( 0 == retainedCount ) {
+					summaryMessage = strings.noDataFound;
+				} else {
+					summaryMessage = strings.noneRemoved;
+					classes = 'notice-warning';
+				}
+			} else {
+				if ( 0 == retainedCount ) {
+					summaryMessage = strings.foundAndRemoved;
+				} else {
+					summaryMessage = strings.someNotRemoved;
+					classes = 'notice-warning';
+				}
+			}
+			appendResultsAfterRow( $requestRow, 'notice-success', summaryMessage, [] );
+		}
+
+		function on_erasure_failure( textStatus, error ) {
+			set_action_state( $action, 'remove_personal_data_failed' );
+			appendResultsAfterRow( $requestRow, 'notice-error', strings.anErrorOccurred, [] );
+		}
+
+		function do_next_erasure( eraserIndex, pageIndex ) {
+			$.ajax( {
+				url: ajaxurl,
+				data: {
+					action: 'wp-privacy-erase-personal-data',
+					eraser: eraserIndex,
+					id: requestID,
+					page: pageIndex,
+					security: nonce,
+				},
+				method: 'post'
+			} ).done( function( response ) {
+				if ( ! response.success ) {
+					on_erasure_failure( 'error', response.data );
+					return;
+				}
+				var responseData = response.data;
+				if ( responseData.num_items_removed ) {
+					removedCount += responseData.num_items_removed;
+				}
+				if ( responseData.num_items_retained ) {
+					retainedCount += responseData.num_items_removed;
+				}
+				if ( responseData.messages ) {
+					messages = messages.concat( responseData.messages );
+				}
+				if ( ! responseData.done ) {
+					setTimeout( do_next_erasure( eraserIndex, pageIndex + 1 ) );
+				} else {
+					if ( eraserIndex < erasersCount ) {
+						setTimeout( do_next_erasure( eraserIndex + 1, 1 ) );
+					} else {
+						on_erasure_done_success();
+					}
+				}
+			} ).fail( function( jqxhr, textStatus, error ) {
+				on_erasure_failure( textStatus, error );
+			} );
+		}
+
+		// And now, let's begin
+		set_action_state( $action, 'remove_personal_data_processing' );
+
+		do_next_erasure( 1, 1 );
+	} )
+} );
Index: src/wp-includes/script-loader.php
===================================================================
--- src/wp-includes/script-loader.php	(revision 42985)
+++ src/wp-includes/script-loader.php	(working copy)
@@ -709,6 +709,15 @@
 		);
 
 		$scripts->add( 'xfn', "/wp-admin/js/xfn$suffix.js", array( 'jquery' ), false, 1 );
+		did_action( 'init' ) && $scripts->localize(
+			'xfn', 'privacyToolsL10n', array(
+				'noDataFound'     => __( 'No personal data was found for this user.' ),
+				'foundAndRemoved' => __( 'All of the personal data found for this user was removed.' ),
+				'noneRemoved'     => __( 'Personal data was found for this user but was not removed.' ),
+				'someNotRemoved'  => __( 'Personal data was found for this user but some of the personal data found was not removed.' ),
+				'anErrorOccurred' => __( 'An error occurred while attempting to find and remove personal data.' ),
+			)
+		);
 
 		$scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable' ), false, 1 );
 		did_action( 'init' ) && $scripts->localize(
