@@ -28,6 +28,7 @@ public function __construct() {
2828 add_filter ( 'bulk_actions-users ' , [ $ this , 'add_bulk_action ' ] );
2929 add_filter ( 'handle_bulk_actions-users ' , [ $ this , 'handle_bulk_action ' ], 10 , 3 );
3030 add_action ( 'admin_init ' , [ $ this , 'handle_single_sync ' ] );
31+ add_action ( 'admin_init ' , [ $ this , 'handle_single_resync ' ] );
3132 add_action ( 'admin_notices ' , [ $ this , 'render_notices ' ] );
3233 }
3334
@@ -61,7 +62,7 @@ public function render_column( string $output, string $column_name, int $user_id
6162 $ environment_id = \WorkOS \Config::get_environment_id ();
6263
6364 if ( $ workos_id ) {
64- return $ this ->render_linked_column ( $ workos_id , $ environment_id );
65+ return $ this ->render_linked_column ( $ user_id , $ workos_id , $ environment_id );
6566 }
6667
6768 return $ this ->render_unlinked_column ( $ user_id );
@@ -70,12 +71,13 @@ public function render_column( string $output, string $column_name, int $user_id
7071 /**
7172 * Render column content for a user linked to WorkOS.
7273 *
74+ * @param int $user_id WordPress user ID.
7375 * @param string $workos_id WorkOS user ID.
7476 * @param string $environment_id WorkOS environment ID.
7577 *
7678 * @return string Column HTML.
7779 */
78- private function render_linked_column ( string $ workos_id , string $ environment_id ): string {
80+ private function render_linked_column ( int $ user_id , string $ workos_id , string $ environment_id ): string {
7981 if ( ! $ environment_id ) {
8082 return '<code> ' . esc_html ( $ workos_id ) . '</code> ' ;
8183 }
@@ -98,6 +100,26 @@ private function render_linked_column( string $workos_id, string $environment_id
98100 esc_url ( $ dashboard_url ),
99101 esc_html__ ( 'View in WorkOS ' , 'workos ' )
100102 );
103+
104+ if ( workos ()->is_enabled () && current_user_can ( 'edit_users ' ) ) {
105+ $ resync_url = wp_nonce_url (
106+ add_query_arg (
107+ [
108+ 'action ' => 'workos_resync_user ' ,
109+ 'user_id ' => $ user_id ,
110+ ],
111+ admin_url ( 'users.php ' )
112+ ),
113+ 'workos_resync_user_ ' . $ user_id
114+ );
115+
116+ $ html .= sprintf (
117+ ' | <span class="resync"><a href="%s">%s</a></span> ' ,
118+ esc_url ( $ resync_url ),
119+ esc_html__ ( 'Re-sync ' , 'workos ' )
120+ );
121+ }
122+
101123 $ html .= '</div> ' ;
102124
103125 return $ html ;
@@ -149,7 +171,8 @@ public function add_bulk_action( array $actions ): array {
149171 return $ actions ;
150172 }
151173
152- $ actions ['workos_bulk_sync ' ] = __ ( 'Sync to WorkOS ' , 'workos ' );
174+ $ actions ['workos_bulk_sync ' ] = __ ( 'Sync to WorkOS ' , 'workos ' );
175+ $ actions ['workos_bulk_resync ' ] = __ ( 'Re-sync from WorkOS ' , 'workos ' );
153176 return $ actions ;
154177 }
155178
@@ -163,14 +186,18 @@ public function add_bulk_action( array $actions ): array {
163186 * @return string Updated redirect URL with result query args.
164187 */
165188 public function handle_bulk_action ( string $ redirect_url , string $ action , array $ user_ids ): string {
166- if ( 'workos_bulk_sync ' !== $ action ) {
189+ if ( ! in_array ( $ action , [ 'workos_bulk_sync ' , ' workos_bulk_resync ' ], true ) ) {
167190 return $ redirect_url ;
168191 }
169192
170193 if ( ! current_user_can ( 'edit_users ' ) ) {
171194 return $ redirect_url ;
172195 }
173196
197+ if ( 'workos_bulk_resync ' === $ action ) {
198+ return $ this ->handle_bulk_resync ( $ redirect_url , $ user_ids );
199+ }
200+
174201 $ synced = 0 ;
175202 $ failed = 0 ;
176203 $ skipped = 0 ;
@@ -211,6 +238,55 @@ public function handle_bulk_action( string $redirect_url, string $action, array
211238 );
212239 }
213240
241+ /**
242+ * Handle the bulk "Re-sync from WorkOS" action.
243+ *
244+ * @param string $redirect_url Redirect URL.
245+ * @param array $user_ids Selected user IDs.
246+ *
247+ * @return string Updated redirect URL with result query args.
248+ */
249+ private function handle_bulk_resync ( string $ redirect_url , array $ user_ids ): string {
250+ $ resynced = 0 ;
251+ $ failed = 0 ;
252+ $ skipped = 0 ;
253+ $ errors = [];
254+
255+ foreach ( $ user_ids as $ user_id ) {
256+ $ user_id = absint ( $ user_id );
257+ $ workos_id = get_user_meta ( $ user_id , '_workos_user_id ' , true );
258+
259+ if ( ! $ workos_id ) {
260+ ++$ skipped ;
261+ continue ;
262+ }
263+
264+ $ result = UserSync::resync_from_workos ( $ user_id );
265+
266+ if ( is_wp_error ( $ result ) ) {
267+ ++$ failed ;
268+ $ errors [] = sprintf (
269+ /* translators: 1: user ID, 2: error message */
270+ __ ( 'User #%1$d: %2$s ' , 'workos ' ),
271+ $ user_id ,
272+ $ result ->get_error_message ()
273+ );
274+ } else {
275+ ++$ resynced ;
276+ }
277+ }
278+
279+ return add_query_arg (
280+ [
281+ 'workos_resynced ' => $ resynced ,
282+ 'workos_failed ' => $ failed ,
283+ 'workos_skipped ' => $ skipped ,
284+ 'workos_errors ' => rawurlencode ( implode ( ' | ' , $ errors ) ),
285+ ],
286+ $ redirect_url
287+ );
288+ }
289+
214290 /**
215291 * Handle a single row-action sync request.
216292 */
@@ -246,6 +322,41 @@ public function handle_single_sync(): void {
246322 exit ;
247323 }
248324
325+ /**
326+ * Handle a single row-action re-sync request.
327+ */
328+ public function handle_single_resync (): void {
329+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below via check_admin_referer().
330+ if ( empty ( $ _GET ['action ' ] ) || 'workos_resync_user ' !== $ _GET ['action ' ] ) {
331+ return ;
332+ }
333+
334+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below via check_admin_referer().
335+ $ user_id = isset ( $ _GET ['user_id ' ] ) ? absint ( $ _GET ['user_id ' ] ) : 0 ;
336+ if ( ! $ user_id ) {
337+ return ;
338+ }
339+
340+ check_admin_referer ( 'workos_resync_user_ ' . $ user_id );
341+
342+ if ( ! current_user_can ( 'edit_users ' ) ) {
343+ wp_die ( esc_html__ ( 'You do not have permission to perform this action. ' , 'workos ' ) );
344+ }
345+
346+ $ result = UserSync::resync_from_workos ( $ user_id );
347+
348+ $ query_args = [];
349+ if ( is_wp_error ( $ result ) ) {
350+ $ query_args ['workos_failed ' ] = 1 ;
351+ $ query_args ['workos_errors ' ] = rawurlencode ( $ result ->get_error_message () );
352+ } else {
353+ $ query_args ['workos_resynced ' ] = 1 ;
354+ }
355+
356+ wp_safe_redirect ( add_query_arg ( $ query_args , admin_url ( 'users.php ' ) ) );
357+ exit ;
358+ }
359+
249360 /**
250361 * Render admin notices from sync operations.
251362 */
@@ -256,13 +367,15 @@ public function render_notices(): void {
256367 }
257368
258369 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only; data is sanitized below.
259- $ synced = isset ( $ _GET ['workos_synced ' ] ) ? absint ( $ _GET ['workos_synced ' ] ) : 0 ;
370+ $ synced = isset ( $ _GET ['workos_synced ' ] ) ? absint ( $ _GET ['workos_synced ' ] ) : 0 ;
371+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only; data is sanitized below.
372+ $ resynced = isset ( $ _GET ['workos_resynced ' ] ) ? absint ( $ _GET ['workos_resynced ' ] ) : 0 ;
260373 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only; data is sanitized below.
261- $ failed = isset ( $ _GET ['workos_failed ' ] ) ? absint ( $ _GET ['workos_failed ' ] ) : 0 ;
374+ $ failed = isset ( $ _GET ['workos_failed ' ] ) ? absint ( $ _GET ['workos_failed ' ] ) : 0 ;
262375 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only; data is sanitized below.
263- $ skipped = isset ( $ _GET ['workos_skipped ' ] ) ? absint ( $ _GET ['workos_skipped ' ] ) : 0 ;
376+ $ skipped = isset ( $ _GET ['workos_skipped ' ] ) ? absint ( $ _GET ['workos_skipped ' ] ) : 0 ;
264377 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only; data is sanitized below.
265- $ errors = isset ( $ _GET ['workos_errors ' ] ) ? sanitize_text_field ( wp_unslash ( $ _GET ['workos_errors ' ] ) ) : '' ;
378+ $ errors = isset ( $ _GET ['workos_errors ' ] ) ? sanitize_text_field ( wp_unslash ( $ _GET ['workos_errors ' ] ) ) : '' ;
266379
267380 if ( $ synced ) {
268381 printf (
@@ -282,12 +395,30 @@ public function render_notices(): void {
282395 );
283396 }
284397
398+ if ( $ resynced ) {
399+ printf (
400+ '<div class="notice notice-success is-dismissible"><p>%s</p></div> ' ,
401+ esc_html (
402+ sprintf (
403+ /* translators: %d: number of users re-synced */
404+ _n (
405+ '%d user re-synced from WorkOS. ' ,
406+ '%d users re-synced from WorkOS. ' ,
407+ $ resynced ,
408+ 'workos '
409+ ),
410+ $ resynced
411+ )
412+ )
413+ );
414+ }
415+
285416 if ( $ failed ) {
286417 $ message = sprintf (
287418 /* translators: %d: number of users that failed to sync */
288419 _n (
289- '%d user failed to sync to WorkOS . ' ,
290- '%d users failed to sync to WorkOS . ' ,
420+ '%d user failed to sync. ' ,
421+ '%d users failed to sync. ' ,
291422 $ failed ,
292423 'workos '
293424 ),
@@ -311,8 +442,8 @@ public function render_notices(): void {
311442 sprintf (
312443 /* translators: %d: number of users skipped */
313444 _n (
314- '%d user skipped (already linked to WorkOS ). ' ,
315- '%d users skipped (already linked to WorkOS ). ' ,
445+ '%d user skipped (not applicable ). ' ,
446+ '%d users skipped (not applicable ). ' ,
316447 $ skipped ,
317448 'workos '
318449 ),
0 commit comments