mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 10:55:00 +08:00
Merge pull request #3059 from woocommerce/PCP-4124-dynamic-logic-for-things-to-do-next-ver4
Settings UI: Add functionality to mark todos as complete on click
This commit is contained in:
commit
2d24240d02
12 changed files with 205 additions and 29 deletions
|
@ -1,6 +1,6 @@
|
||||||
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
|
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { STORE_NAME as TODOS_STORE_NAME } from '../../../data/todos';
|
import { STORE_NAME as TODOS_STORE_NAME } from '../../../data/todos';
|
||||||
|
|
||||||
const TodoSettingsBlock = ( {
|
const TodoSettingsBlock = ( {
|
||||||
|
@ -21,6 +21,8 @@ const TodoSettingsBlock = ( {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { completeOnClick } = useDispatch( TODOS_STORE_NAME );
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( dismissedTodos.length === 0 ) {
|
if ( dismissedTodos.length === 0 ) {
|
||||||
setDismissingIds( new Set() );
|
setDismissingIds( new Set() );
|
||||||
|
@ -41,6 +43,26 @@ const TodoSettingsBlock = ( {
|
||||||
}, 300 );
|
}, 300 );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClick = async ( todo ) => {
|
||||||
|
if ( todo.action.type === 'tab' ) {
|
||||||
|
const tabId = TAB_IDS[ todo.action.tab.toUpperCase() ];
|
||||||
|
await selectTab( tabId, todo.action.section );
|
||||||
|
} else if ( todo.action.type === 'external' ) {
|
||||||
|
window.open( todo.action.url, '_blank' );
|
||||||
|
// If it has completeOnClick flag, trigger the action
|
||||||
|
if ( todo.action.completeOnClick === true ) {
|
||||||
|
await completeOnClick( todo.id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( todo.action.modal ) {
|
||||||
|
setActiveModal( todo.action.modal );
|
||||||
|
}
|
||||||
|
if ( todo.action.highlight ) {
|
||||||
|
setActiveHighlight( todo.action.highlight );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Filter out dismissed todos for display
|
// Filter out dismissed todos for display
|
||||||
const visibleTodos = todosData.filter(
|
const visibleTodos = todosData.filter(
|
||||||
( todo ) => ! dismissedTodos.includes( todo.id )
|
( todo ) => ! dismissedTodos.includes( todo.id )
|
||||||
|
@ -59,22 +81,7 @@ const TodoSettingsBlock = ( {
|
||||||
isCompleted={ completedTodos.includes( todo.id ) }
|
isCompleted={ completedTodos.includes( todo.id ) }
|
||||||
isDismissing={ dismissingIds.has( todo.id ) }
|
isDismissing={ dismissingIds.has( todo.id ) }
|
||||||
onDismiss={ ( e ) => handleDismiss( todo.id, e ) }
|
onDismiss={ ( e ) => handleDismiss( todo.id, e ) }
|
||||||
onClick={ async () => {
|
onClick={ () => handleClick( todo ) }
|
||||||
if ( todo.action.type === 'tab' ) {
|
|
||||||
const tabId =
|
|
||||||
TAB_IDS[ todo.action.tab.toUpperCase() ];
|
|
||||||
await selectTab( tabId, todo.action.section );
|
|
||||||
} else if ( todo.action.type === 'external' ) {
|
|
||||||
window.open( todo.action.url, '_blank' );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( todo.action.modal ) {
|
|
||||||
setActiveModal( todo.action.modal );
|
|
||||||
}
|
|
||||||
if ( todo.action.highlight ) {
|
|
||||||
setActiveHighlight( todo.action.highlight );
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
/>
|
/>
|
||||||
) ) }
|
) ) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,4 +17,5 @@ export default {
|
||||||
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
|
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
|
||||||
DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA',
|
DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA',
|
||||||
DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS',
|
DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS',
|
||||||
|
DO_COMPLETE_ONCLICK: 'TODOS:DO_COMPLETE_ONCLICK',
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,8 +39,7 @@ export const resetDismissedTodos = function* () {
|
||||||
const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS };
|
const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS };
|
||||||
|
|
||||||
if ( result && result.success ) {
|
if ( result && result.success ) {
|
||||||
// After successful reset, fetch fresh todos
|
yield setDismissedTodos( [] );
|
||||||
yield fetchTodos();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -50,3 +49,19 @@ export const setCompletedTodos = ( completedTodos ) => ( {
|
||||||
type: ACTION_TYPES.SET_COMPLETED_TODOS,
|
type: ACTION_TYPES.SET_COMPLETED_TODOS,
|
||||||
payload: completedTodos,
|
payload: completedTodos,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
export const completeOnClick = function* ( todoId ) {
|
||||||
|
const result = yield {
|
||||||
|
type: ACTION_TYPES.DO_COMPLETE_ONCLICK,
|
||||||
|
todoId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( result && result.success ) {
|
||||||
|
// Set transient completed state for visual feedback
|
||||||
|
const currentTransientCompleted =
|
||||||
|
yield select( STORE_NAME ).getCompletedTodos();
|
||||||
|
yield setCompletedTodos( [ ...currentTransientCompleted, todoId ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
|
@ -9,3 +9,4 @@ export const REST_PATH = '/wc/v3/wc_paypal/todos';
|
||||||
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/todos';
|
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/todos';
|
||||||
export const REST_RESET_DISMISSED_TODOS_PATH =
|
export const REST_RESET_DISMISSED_TODOS_PATH =
|
||||||
'/wc/v3/wc_paypal/reset-dismissed-todos';
|
'/wc/v3/wc_paypal/reset-dismissed-todos';
|
||||||
|
export const REST_COMPLETE_ONCLICK_PATH = '/wc/v3/wc_paypal/complete-onclick';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
REST_PATH,
|
REST_PATH,
|
||||||
REST_PERSIST_PATH,
|
REST_PERSIST_PATH,
|
||||||
REST_RESET_DISMISSED_TODOS_PATH,
|
REST_RESET_DISMISSED_TODOS_PATH,
|
||||||
|
REST_COMPLETE_ONCLICK_PATH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import ACTION_TYPES from './action-types';
|
import ACTION_TYPES from './action-types';
|
||||||
|
|
||||||
|
@ -44,4 +45,21 @@ export const controls = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async [ ACTION_TYPES.DO_COMPLETE_ONCLICK ]( { todoId } ) {
|
||||||
|
try {
|
||||||
|
const response = await apiFetch( {
|
||||||
|
path: REST_COMPLETE_ONCLICK_PATH,
|
||||||
|
method: 'POST',
|
||||||
|
data: { todoId },
|
||||||
|
} );
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch ( e ) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e,
|
||||||
|
message: e.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,7 @@ const defaultTransient = Object.freeze( {
|
||||||
const defaultPersistent = Object.freeze( {
|
const defaultPersistent = Object.freeze( {
|
||||||
todos: [],
|
todos: [],
|
||||||
dismissedTodos: [],
|
dismissedTodos: [],
|
||||||
|
completedOnClickTodos: [],
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Reducer logic.
|
// Reducer logic.
|
||||||
|
|
|
@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
|
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\CompleteOnClickEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
|
||||||
|
@ -265,7 +266,8 @@ return array(
|
||||||
},
|
},
|
||||||
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
|
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
|
||||||
return new TodosDefinition(
|
return new TodosDefinition(
|
||||||
$container->get( 'settings.service.todos_eligibilities' )
|
$container->get( 'settings.service.todos_eligibilities' ),
|
||||||
|
$container->get( 'settings.data.general' )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'settings.service.todos_eligibilities' => static function( ContainerInterface $container ): TodosEligibilityService {
|
'settings.service.todos_eligibilities' => static function( ContainerInterface $container ): TodosEligibilityService {
|
||||||
|
@ -314,4 +316,7 @@ return array(
|
||||||
'settings.rest.reset_dismissed_todos' => static function( ContainerInterface $container ): ResetDismissedTodosEndpoint {
|
'settings.rest.reset_dismissed_todos' => static function( ContainerInterface $container ): ResetDismissedTodosEndpoint {
|
||||||
return new ResetDismissedTodosEndpoint();
|
return new ResetDismissedTodosEndpoint();
|
||||||
},
|
},
|
||||||
|
'settings.rest.complete_onclick' => static function( ContainerInterface $container ): CompleteOnClickEndpoint {
|
||||||
|
return new CompleteOnClickEndpoint();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||||
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
|
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
|
||||||
|
|
||||||
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
|
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class TodosDefinition
|
* Class TodosDefinition
|
||||||
|
@ -26,13 +27,25 @@ class TodosDefinition {
|
||||||
*/
|
*/
|
||||||
protected TodosEligibilityService $eligibilities;
|
protected TodosEligibilityService $eligibilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general settings service.
|
||||||
|
*
|
||||||
|
* @var GeneralSettings
|
||||||
|
*/
|
||||||
|
protected GeneralSettings $settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*
|
*
|
||||||
* @param TodosEligibilityService $eligibilities The todos eligibility service.
|
* @param TodosEligibilityService $eligibilities The todos eligibility service.
|
||||||
|
* @param GeneralSettings $settings The general settings service.
|
||||||
*/
|
*/
|
||||||
public function __construct( TodosEligibilityService $eligibilities ) {
|
public function __construct(
|
||||||
|
TodosEligibilityService $eligibilities,
|
||||||
|
GeneralSettings $settings
|
||||||
|
) {
|
||||||
$this->eligibilities = $eligibilities;
|
$this->eligibilities = $eligibilities;
|
||||||
|
$this->settings = $settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,9 +123,11 @@ class TodosDefinition {
|
||||||
'description' => __( 'To enable Apple Pay, you must register your domain with PayPal', 'woocommerce-paypal-payments' ),
|
'description' => __( 'To enable Apple Pay, you must register your domain with PayPal', 'woocommerce-paypal-payments' ),
|
||||||
'isEligible' => $eligibility_checks['register_domain_apple_pay'],
|
'isEligible' => $eligibility_checks['register_domain_apple_pay'],
|
||||||
'action' => array(
|
'action' => array(
|
||||||
'type' => 'tab',
|
'type' => 'external',
|
||||||
'tab' => 'overview',
|
'url' => $this->settings->is_sandbox_merchant()
|
||||||
'section' => 'apple_pay',
|
? 'https://www.sandbox.paypal.com/uccservicing/apm/applepay'
|
||||||
|
: 'https://www.paypal.com/uccservicing/apm/applepay',
|
||||||
|
'completeOnClick' => true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
'add_digital_wallets' => array(
|
'add_digital_wallets' => array(
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CompleteOnClickEndpoint class file.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the REST endpoint for marking todos as completed on click.
|
||||||
|
*
|
||||||
|
* This file is part of the WooCommerce PayPal Commerce Settings module and provides
|
||||||
|
* functionality for tracking which todos have been completed via click actions.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||||
|
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CompleteOnClickEndpoint
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for marking todos as completed when clicked.
|
||||||
|
* Extends the base RestEndpoint class to provide specific todo completion functionality.
|
||||||
|
*/
|
||||||
|
class CompleteOnClickEndpoint extends RestEndpoint {
|
||||||
|
/**
|
||||||
|
* The base URL for the REST endpoint.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'complete-onclick';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the routes for the complete-onclick endpoint.
|
||||||
|
*
|
||||||
|
* Sets up the REST API route for handling todo completion via POST requests.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => array( $this, 'complete_onclick' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the completion of a todo item via click.
|
||||||
|
*
|
||||||
|
* Processes the POST request to mark a specific todo as completed,
|
||||||
|
* updating the stored settings accordingly.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request The incoming REST request object.
|
||||||
|
* @return WP_REST_Response The REST response indicating success or failure.
|
||||||
|
*/
|
||||||
|
public function complete_onclick( WP_REST_Request $request ): WP_REST_Response {
|
||||||
|
$todo_id = $request->get_param( 'todoId' );
|
||||||
|
|
||||||
|
if ( ! $todo_id ) {
|
||||||
|
return $this->return_error( __( 'Todo ID is required.', 'woocommerce-paypal-payments' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = get_option( 'ppcp-settings', array() );
|
||||||
|
|
||||||
|
if ( ! isset( $settings['completedOnClickTodos'] ) ) {
|
||||||
|
$settings['completedOnClickTodos'] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $todo_id, $settings['completedOnClickTodos'], true ) ) {
|
||||||
|
$settings['completedOnClickTodos'][] = $todo_id;
|
||||||
|
$update_result = update_option( 'ppcp-settings', $settings );
|
||||||
|
|
||||||
|
if ( ! $update_result ) {
|
||||||
|
return $this->return_error( __( 'Failed to mark todo as completed on click.', 'woocommerce-paypal-payments' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->return_success(
|
||||||
|
array(
|
||||||
|
'message' => __( 'Todo marked as completed on click successfully.', 'woocommerce-paypal-payments' ),
|
||||||
|
'todoId' => $todo_id,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,11 @@ class ResetDismissedTodosEndpoint extends RestEndpoint {
|
||||||
$settings = get_option( 'ppcp-settings', array() );
|
$settings = get_option( 'ppcp-settings', array() );
|
||||||
|
|
||||||
$settings['dismissedTodos'] = array();
|
$settings['dismissedTodos'] = array();
|
||||||
$update_result = update_option( 'ppcp-settings', $settings );
|
|
||||||
|
// Clear the completedOnClickTodos for testing purposes.
|
||||||
|
// $settings['completedOnClickTodos'] = array();.
|
||||||
|
|
||||||
|
$update_result = update_option( 'ppcp-settings', $settings );
|
||||||
|
|
||||||
if ( ! $update_result ) {
|
if ( ! $update_result ) {
|
||||||
return $this->return_error( __( 'Failed to reset dismissed todos.', 'woocommerce-paypal-payments' ) );
|
return $this->return_error( __( 'Failed to reset dismissed todos.', 'woocommerce-paypal-payments' ) );
|
||||||
|
|
|
@ -103,11 +103,22 @@ class TodosRestEndpoint extends RestEndpoint {
|
||||||
* @return WP_REST_Response The response containing todos data.
|
* @return WP_REST_Response The response containing todos data.
|
||||||
*/
|
*/
|
||||||
public function get_todos(): WP_REST_Response {
|
public function get_todos(): WP_REST_Response {
|
||||||
$settings = get_option( 'ppcp-settings', array() );
|
$settings = get_option( 'ppcp-settings', array() );
|
||||||
$dismissed_ids = $settings['dismissedTodos'] ?? array();
|
$dismissed_ids = $settings['dismissedTodos'] ?? array();
|
||||||
|
$completed_onclick_ids = $settings['completedOnClickTodos'] ?? array();
|
||||||
|
|
||||||
$todos = array();
|
$todos = array();
|
||||||
foreach ( $this->todos_definition->get() as $id => $todo ) {
|
foreach ( $this->todos_definition->get() as $id => $todo ) {
|
||||||
|
// Skip if todo has completeOnClick flag and is in completed list.
|
||||||
|
if (
|
||||||
|
in_array( $id, $completed_onclick_ids, true ) &&
|
||||||
|
isset( $todo['action']['completeOnClick'] ) &&
|
||||||
|
$todo['action']['completeOnClick'] === true
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check eligibility and add to todos if eligible.
|
||||||
if ( $todo['isEligible']() ) {
|
if ( $todo['isEligible']() ) {
|
||||||
$todos[] = array_merge(
|
$todos[] = array_merge(
|
||||||
array( 'id' => $id ),
|
array( 'id' => $id ),
|
||||||
|
@ -118,8 +129,9 @@ class TodosRestEndpoint extends RestEndpoint {
|
||||||
|
|
||||||
return $this->return_success(
|
return $this->return_success(
|
||||||
array(
|
array(
|
||||||
'todos' => $todos,
|
'todos' => $todos,
|
||||||
'dismissedTodos' => $dismissed_ids,
|
'dismissedTodos' => $dismissed_ids,
|
||||||
|
'completedOnClickTodos' => $completed_onclick_ids,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
||||||
'todos' => $container->get( 'settings.rest.todos' ),
|
'todos' => $container->get( 'settings.rest.todos' ),
|
||||||
'reset_dismissed_todos' => $container->get( 'settings.rest.reset_dismissed_todos' ),
|
'reset_dismissed_todos' => $container->get( 'settings.rest.reset_dismissed_todos' ),
|
||||||
'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ),
|
'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ),
|
||||||
|
'complete_onclick' => $container->get( 'settings.rest.complete_onclick' ),
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $endpoints as $endpoint ) {
|
foreach ( $endpoints as $endpoint ) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue