mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-03 08:37:53 +08:00
Separate message rendering, fix some message rendering issues
This commit is contained in:
parent
dbe4e82707
commit
e5513d51de
6 changed files with 128 additions and 74 deletions
|
@ -19,6 +19,7 @@ import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
|
||||||
import FormSaver from './modules/Helper/FormSaver';
|
import FormSaver from './modules/Helper/FormSaver';
|
||||||
import FormValidator from "./modules/Helper/FormValidator";
|
import FormValidator from "./modules/Helper/FormValidator";
|
||||||
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
|
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
|
||||||
|
import MessagesBootstrap from "./modules/ContextBootstrap/MessagesBootstap";
|
||||||
|
|
||||||
// TODO: could be a good idea to have a separate spinner for each gateway,
|
// TODO: could be a good idea to have a separate spinner for each gateway,
|
||||||
// but I think we care mainly about the script loading, so one spinner should be enough.
|
// but I think we care mainly about the script loading, so one spinner should be enough.
|
||||||
|
@ -158,7 +159,6 @@ const bootstrap = () => {
|
||||||
const singleProductBootstrap = new SingleProductBootstap(
|
const singleProductBootstrap = new SingleProductBootstap(
|
||||||
PayPalCommerceGateway,
|
PayPalCommerceGateway,
|
||||||
renderer,
|
renderer,
|
||||||
messageRenderer,
|
|
||||||
errorHandler,
|
errorHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -169,7 +169,6 @@ const bootstrap = () => {
|
||||||
const cartBootstrap = new CartBootstrap(
|
const cartBootstrap = new CartBootstrap(
|
||||||
PayPalCommerceGateway,
|
PayPalCommerceGateway,
|
||||||
renderer,
|
renderer,
|
||||||
messageRenderer,
|
|
||||||
errorHandler,
|
errorHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -180,7 +179,6 @@ const bootstrap = () => {
|
||||||
const checkoutBootstap = new CheckoutBootstap(
|
const checkoutBootstap = new CheckoutBootstap(
|
||||||
PayPalCommerceGateway,
|
PayPalCommerceGateway,
|
||||||
renderer,
|
renderer,
|
||||||
messageRenderer,
|
|
||||||
spinner,
|
spinner,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
);
|
);
|
||||||
|
@ -199,6 +197,11 @@ const bootstrap = () => {
|
||||||
payNowBootstrap.init();
|
payNowBootstrap.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messagesBootstrap = new MessagesBootstrap(
|
||||||
|
PayPalCommerceGateway,
|
||||||
|
messageRenderer,
|
||||||
|
);
|
||||||
|
messagesBootstrap.init();
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMessages = () => {
|
const hasMessages = () => {
|
||||||
|
|
|
@ -2,12 +2,10 @@ import CartActionHandler from '../ActionHandler/CartActionHandler';
|
||||||
import BootstrapHelper from "../Helper/BootstrapHelper";
|
import BootstrapHelper from "../Helper/BootstrapHelper";
|
||||||
|
|
||||||
class CartBootstrap {
|
class CartBootstrap {
|
||||||
constructor(gateway, renderer, messages, errorHandler) {
|
constructor(gateway, renderer, errorHandler) {
|
||||||
this.gateway = gateway;
|
this.gateway = gateway;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.messages = messages;
|
|
||||||
this.errorHandler = errorHandler;
|
this.errorHandler = errorHandler;
|
||||||
this.lastAmount = this.gateway.messages.amount;
|
|
||||||
|
|
||||||
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
|
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
|
||||||
this.handleButtonStatus();
|
this.handleButtonStatus();
|
||||||
|
@ -15,16 +13,16 @@ class CartBootstrap {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (!this.shouldRender()) {
|
if (this.shouldRender()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.handleButtonStatus();
|
|
||||||
|
|
||||||
jQuery(document.body).on('updated_cart_totals updated_checkout', () => {
|
|
||||||
this.render();
|
this.render();
|
||||||
this.handleButtonStatus();
|
this.handleButtonStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(document.body).on('updated_cart_totals updated_checkout', () => {
|
||||||
|
if (this.shouldRender()) {
|
||||||
|
this.render();
|
||||||
|
this.handleButtonStatus();
|
||||||
|
}
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
this.gateway.ajax.cart_script_params.endpoint,
|
this.gateway.ajax.cart_script_params.endpoint,
|
||||||
|
@ -49,16 +47,19 @@ class CartBootstrap {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle button status
|
// handle button status
|
||||||
if (result.data.button || result.data.messages) {
|
const newData = {};
|
||||||
this.gateway.button = result.data.button;
|
if (result.data.button) {
|
||||||
this.gateway.messages = result.data.messages;
|
newData.button = result.data.button;
|
||||||
|
}
|
||||||
|
if (result.data.messages) {
|
||||||
|
newData.messages = result.data.messages;
|
||||||
|
}
|
||||||
|
if (newData) {
|
||||||
|
BootstrapHelper.updateScriptData(this, newData);
|
||||||
this.handleButtonStatus();
|
this.handleButtonStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastAmount !== result.data.amount) {
|
jQuery(document.body).trigger('ppcp_cart_total_updated', [result.data.amount]);
|
||||||
this.lastAmount = result.data.amount;
|
|
||||||
this.messages.renderWithAmount(this.lastAmount);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -76,6 +77,10 @@ class CartBootstrap {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.shouldRender()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const actionHandler = new CartActionHandler(
|
const actionHandler = new CartActionHandler(
|
||||||
PayPalCommerceGateway,
|
PayPalCommerceGateway,
|
||||||
this.errorHandler,
|
this.errorHandler,
|
||||||
|
@ -99,7 +104,7 @@ class CartBootstrap {
|
||||||
actionHandler.configuration()
|
actionHandler.configuration()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.messages.renderWithAmount(this.lastAmount);
|
jQuery(document.body).trigger('ppcp_cart_rendered');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,11 @@ import {
|
||||||
import BootstrapHelper from "../Helper/BootstrapHelper";
|
import BootstrapHelper from "../Helper/BootstrapHelper";
|
||||||
|
|
||||||
class CheckoutBootstap {
|
class CheckoutBootstap {
|
||||||
constructor(gateway, renderer, messages, spinner, errorHandler) {
|
constructor(gateway, renderer, spinner, errorHandler) {
|
||||||
this.gateway = gateway;
|
this.gateway = gateway;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.messages = messages;
|
|
||||||
this.spinner = spinner;
|
this.spinner = spinner;
|
||||||
this.errorHandler = errorHandler;
|
this.errorHandler = errorHandler;
|
||||||
this.lastAmount = this.gateway.messages.amount;
|
|
||||||
|
|
||||||
this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR;
|
this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR;
|
||||||
|
|
||||||
|
@ -37,7 +35,7 @@ class CheckoutBootstap {
|
||||||
this.render()
|
this.render()
|
||||||
this.handleButtonStatus();
|
this.handleButtonStatus();
|
||||||
|
|
||||||
if (this.shouldRenderMessages()) { // currently we need amount only for Pay Later
|
if (this.shouldShowMessages() && document.querySelector(this.gateway.messages.wrapper)) { // currently we need amount only for Pay Later
|
||||||
fetch(
|
fetch(
|
||||||
this.gateway.ajax.cart_script_params.endpoint,
|
this.gateway.ajax.cart_script_params.endpoint,
|
||||||
{
|
{
|
||||||
|
@ -51,10 +49,7 @@ class CheckoutBootstap {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastAmount !== result.data.amount) {
|
jQuery(document.body).trigger('ppcp_checkout_total_updated', [result.data.amount]);
|
||||||
this.lastAmount = result.data.amount;
|
|
||||||
this.updateUi();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -69,6 +64,12 @@ class CheckoutBootstap {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jQuery(document).on('ppcp_should_show_messages', (e, data) => {
|
||||||
|
if (!this.shouldShowMessages()) {
|
||||||
|
data.result = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.updateUi();
|
this.updateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,16 +139,11 @@ class CheckoutBootstap {
|
||||||
setVisibleByClass(this.standardOrderButtonSelector, (isPaypal && isFreeTrial && hasVaultedPaypal) || isNotOurGateway || isSavedCard, 'ppcp-hidden');
|
setVisibleByClass(this.standardOrderButtonSelector, (isPaypal && isFreeTrial && hasVaultedPaypal) || isNotOurGateway || isSavedCard, 'ppcp-hidden');
|
||||||
setVisible('.ppcp-vaulted-paypal-details', isPaypal);
|
setVisible('.ppcp-vaulted-paypal-details', isPaypal);
|
||||||
setVisible(this.gateway.button.wrapper, isPaypal && !(isFreeTrial && hasVaultedPaypal));
|
setVisible(this.gateway.button.wrapper, isPaypal && !(isFreeTrial && hasVaultedPaypal));
|
||||||
setVisible(this.gateway.messages.wrapper, isPaypal && !isFreeTrial);
|
|
||||||
setVisible(this.gateway.hosted_fields.wrapper, isCard && !isSavedCard);
|
setVisible(this.gateway.hosted_fields.wrapper, isCard && !isSavedCard);
|
||||||
for (const [gatewayId, wrapper] of Object.entries(paypalButtonWrappers)) {
|
for (const [gatewayId, wrapper] of Object.entries(paypalButtonWrappers)) {
|
||||||
setVisible(wrapper, gatewayId === currentPaymentMethod);
|
setVisible(wrapper, gatewayId === currentPaymentMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldRenderMessages()) {
|
|
||||||
this.messages.renderWithAmount(this.lastAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCard) {
|
if (isCard) {
|
||||||
if (isSavedCard) {
|
if (isSavedCard) {
|
||||||
this.disableCreditCardFields();
|
this.disableCreditCardFields();
|
||||||
|
@ -155,12 +151,13 @@ class CheckoutBootstap {
|
||||||
this.enableCreditCardFields();
|
this.enableCreditCardFields();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jQuery(document.body).trigger('ppcp_checkout_rendered');
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRenderMessages() {
|
shouldShowMessages() {
|
||||||
return getCurrentPaymentMethod() === PaymentMethods.PAYPAL
|
return getCurrentPaymentMethod() === PaymentMethods.PAYPAL
|
||||||
&& !PayPalCommerceGateway.is_free_trial_cart
|
&& !PayPalCommerceGateway.is_free_trial_cart;
|
||||||
&& this.messages.shouldRender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disableCreditCardFields() {
|
disableCreditCardFields() {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import {setVisible} from "../Helper/Hiding";
|
||||||
|
|
||||||
|
class MessagesBootstrap {
|
||||||
|
constructor(gateway, messageRenderer) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
this.renderer = messageRenderer;
|
||||||
|
this.lastAmount = this.gateway.messages.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
jQuery(document.body).on('ppcp_cart_rendered ppcp_checkout_rendered', () => {
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
jQuery(document.body).on('ppcp_script_data_changed', (e, data) => {
|
||||||
|
this.gateway = data;
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
jQuery(document.body).on('ppcp_cart_total_updated ppcp_checkout_total_updated ppcp_product_total_updated', (e, amount) => {
|
||||||
|
if (this.lastAmount !== amount) {
|
||||||
|
this.lastAmount = amount;
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShow() {
|
||||||
|
if (this.gateway.messages.is_hidden === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = {result: true}
|
||||||
|
jQuery(document.body).trigger('ppcp_should_show_messages', [eventData]);
|
||||||
|
return eventData.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRender() {
|
||||||
|
return this.shouldShow() && this.renderer.shouldRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
setVisible(this.gateway.messages.wrapper, this.shouldShow());
|
||||||
|
|
||||||
|
if (!this.shouldRender()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.renderWithAmount(this.lastAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessagesBootstrap;
|
|
@ -6,12 +6,12 @@ import {loadPaypalJsScript} from "../Helper/ScriptLoading";
|
||||||
import {getPlanIdFromVariation} from "../Helper/Subscriptions"
|
import {getPlanIdFromVariation} from "../Helper/Subscriptions"
|
||||||
import SimulateCart from "../Helper/SimulateCart";
|
import SimulateCart from "../Helper/SimulateCart";
|
||||||
import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
|
import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
|
||||||
|
import merge from "deepmerge";
|
||||||
|
|
||||||
class SingleProductBootstap {
|
class SingleProductBootstap {
|
||||||
constructor(gateway, renderer, messages, errorHandler) {
|
constructor(gateway, renderer, errorHandler) {
|
||||||
this.gateway = gateway;
|
this.gateway = gateway;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.messages = messages;
|
|
||||||
this.errorHandler = errorHandler;
|
this.errorHandler = errorHandler;
|
||||||
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
|
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
|
||||||
this.formSelector = 'form.cart';
|
this.formSelector = 'form.cart';
|
||||||
|
@ -36,7 +36,6 @@ class SingleProductBootstap {
|
||||||
if (!this.shouldRender()) {
|
if (!this.shouldRender()) {
|
||||||
this.renderer.disableSmartButtons(this.gateway.button.wrapper);
|
this.renderer.disableSmartButtons(this.gateway.button.wrapper);
|
||||||
hide(this.gateway.button.wrapper, this.formSelector);
|
hide(this.gateway.button.wrapper, this.formSelector);
|
||||||
hide(this.gateway.messages.wrapper);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +43,6 @@ class SingleProductBootstap {
|
||||||
|
|
||||||
this.renderer.enableSmartButtons(this.gateway.button.wrapper);
|
this.renderer.enableSmartButtons(this.gateway.button.wrapper);
|
||||||
show(this.gateway.button.wrapper);
|
show(this.gateway.button.wrapper);
|
||||||
show(this.gateway.messages.wrapper);
|
|
||||||
|
|
||||||
this.handleButtonStatus();
|
this.handleButtonStatus();
|
||||||
}
|
}
|
||||||
|
@ -78,6 +76,12 @@ class SingleProductBootstap {
|
||||||
.observe(addToCartButton, { attributes : true });
|
.observe(addToCartButton, { attributes : true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jQuery(document).on('ppcp_should_show_messages', (e, data) => {
|
||||||
|
if (!this.shouldRender()) {
|
||||||
|
data.result = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.shouldRender()) {
|
if (!this.shouldRender()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -232,7 +236,18 @@ class SingleProductBootstap {
|
||||||
this.gateway.ajax.simulate_cart.nonce,
|
this.gateway.ajax.simulate_cart.nonce,
|
||||||
)).simulate((data) => {
|
)).simulate((data) => {
|
||||||
|
|
||||||
this.messages.renderWithAmount(data.total);
|
jQuery(document.body).trigger('ppcp_product_total_updated', [data.total]);
|
||||||
|
|
||||||
|
let newData = {};
|
||||||
|
if (typeof data.button.is_disabled === 'boolean') {
|
||||||
|
newData = merge(newData, {button: {is_disabled: data.button.is_disabled}});
|
||||||
|
}
|
||||||
|
if (typeof data.messages.is_hidden === 'boolean') {
|
||||||
|
newData = merge(newData, {messages: {is_hidden: data.messages.is_hidden}});
|
||||||
|
}
|
||||||
|
if (newData) {
|
||||||
|
BootstrapHelper.updateScriptData(this, newData);
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.gateway.single_product_buttons_enabled !== '1' ) {
|
if ( this.gateway.single_product_buttons_enabled !== '1' ) {
|
||||||
return;
|
return;
|
||||||
|
@ -260,13 +275,6 @@ class SingleProductBootstap {
|
||||||
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
|
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.button.is_disabled === 'boolean') {
|
|
||||||
this.gateway.button.is_disabled = data.button.is_disabled;
|
|
||||||
}
|
|
||||||
if (typeof data.messages.is_hidden === 'boolean') {
|
|
||||||
this.gateway.messages.is_hidden = data.messages.is_hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleButtonStatus(false);
|
this.handleButtonStatus(false);
|
||||||
|
|
||||||
}, products);
|
}, products);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {disable, enable, isDisabled} from "./ButtonDisabler";
|
import {disable, enable, isDisabled} from "./ButtonDisabler";
|
||||||
import {hide, show} from "./Hiding";
|
import merge from "deepmerge";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common Bootstrap methods to avoid code repetition.
|
* Common Bootstrap methods to avoid code repetition.
|
||||||
|
@ -9,15 +9,6 @@ export default class BootstrapHelper {
|
||||||
static handleButtonStatus(bs, options) {
|
static handleButtonStatus(bs, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
options.wrapper = options.wrapper || bs.gateway.button.wrapper;
|
options.wrapper = options.wrapper || bs.gateway.button.wrapper;
|
||||||
options.messagesWrapper = options.messagesWrapper || bs.gateway.messages.wrapper;
|
|
||||||
options.skipMessages = options.skipMessages || false;
|
|
||||||
|
|
||||||
// Handle messages hide / show
|
|
||||||
if (this.shouldShowMessages(bs, options)) {
|
|
||||||
show(options.messagesWrapper);
|
|
||||||
} else {
|
|
||||||
hide(options.messagesWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasDisabled = isDisabled(options.wrapper);
|
const wasDisabled = isDisabled(options.wrapper);
|
||||||
const shouldEnable = bs.shouldEnable();
|
const shouldEnable = bs.shouldEnable();
|
||||||
|
@ -26,17 +17,9 @@ export default class BootstrapHelper {
|
||||||
if (shouldEnable && wasDisabled) {
|
if (shouldEnable && wasDisabled) {
|
||||||
bs.renderer.enableSmartButtons(options.wrapper);
|
bs.renderer.enableSmartButtons(options.wrapper);
|
||||||
enable(options.wrapper);
|
enable(options.wrapper);
|
||||||
|
|
||||||
if (!options.skipMessages) {
|
|
||||||
enable(options.messagesWrapper);
|
|
||||||
}
|
|
||||||
} else if (!shouldEnable && !wasDisabled) {
|
} else if (!shouldEnable && !wasDisabled) {
|
||||||
bs.renderer.disableSmartButtons(options.wrapper);
|
bs.renderer.disableSmartButtons(options.wrapper);
|
||||||
disable(options.wrapper, options.formSelector || null);
|
disable(options.wrapper, options.formSelector || null);
|
||||||
|
|
||||||
if (!options.skipMessages) {
|
|
||||||
disable(options.messagesWrapper);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasDisabled !== !shouldEnable) {
|
if (wasDisabled !== !shouldEnable) {
|
||||||
|
@ -54,12 +37,15 @@ export default class BootstrapHelper {
|
||||||
&& options.isDisabled !== true;
|
&& options.isDisabled !== true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static shouldShowMessages(bs, options) {
|
static updateScriptData(bs, newData) {
|
||||||
options = options || {};
|
const newObj = merge(bs.gateway, newData);
|
||||||
if (typeof options.isMessagesHidden === 'undefined') {
|
|
||||||
options.isMessagesHidden = bs.gateway.messages.is_hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.isMessagesHidden !== true;
|
const isChanged = JSON.stringify(bs.gateway) !== JSON.stringify(newObj);
|
||||||
|
|
||||||
|
bs.gateway = newObj;
|
||||||
|
|
||||||
|
if (isChanged) {
|
||||||
|
jQuery(document.body).trigger('ppcp_script_data_changed', [newObj]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue