WooCommerce API nodes for Node-RED
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "node-red-contrib-nextcloud-woocommerce",
|
||||
"version": "1.0.0",
|
||||
"description": "Node-RED nodes for WooCommerce REST API integration",
|
||||
"keywords": ["node-red", "woocommerce", "ecommerce", "orders"],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://kode.hannesahlin.se/n3wk/nextcloud-nodered-woocommerce"
|
||||
},
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
"woocommerce-config": "woocommerce-config.js",
|
||||
"woocommerce-order": "woocommerce-order.js",
|
||||
"woocommerce-customer": "woocommerce-customer.js",
|
||||
"woocommerce-product": "woocommerce-product.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("woocommerce-config", {
|
||||
category: "config",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
baseUrl: { value: "https://shop.hanssonit.se/wp-json/wc/v3", required: true }
|
||||
},
|
||||
credentials: {
|
||||
consumerKey: { type: "text" },
|
||||
consumerSecret: { type: "password" }
|
||||
},
|
||||
label: function () {
|
||||
return this.name || this.baseUrl || "WooCommerce";
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="woocommerce-config">
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-config-input-name" placeholder="WooCommerce Config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-baseUrl"><i class="fa fa-server"></i> Base URL</label>
|
||||
<input type="text" id="node-config-input-baseUrl" placeholder="https://shop.hanssonit.se/wp-json/wc/v3">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-consumerKey"><i class="fa fa-key"></i> Consumer Key</label>
|
||||
<input type="text" id="node-config-input-consumerKey" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-consumerSecret"><i class="fa fa-lock"></i> Consumer Secret</label>
|
||||
<input type="password" id="node-config-input-consumerSecret" autocomplete="off">
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:4px;">
|
||||
Create API key at: WooCommerce → Settings → Advanced → REST API.<br>
|
||||
Key must have <b>Read</b> permission (preferably Read/Write).<br>
|
||||
User bound to key must be <b>Administrator</b> or equivalent.
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="woocommerce-config">
|
||||
<p>WooCommerce REST API configuration node.</p>
|
||||
<h3>Details</h3>
|
||||
<p>Configure your WooCommerce store connection.</p>
|
||||
<ul>
|
||||
<li><b>Base URL</b> – Must be <code>https://shop.hanssonit.se/wp-json/wc/v3</code></li>
|
||||
<li><b>Consumer Key</b> – From WooCommerce → Settings → Advanced → REST API</li>
|
||||
<li><b>Consumer Secret</b> – Paired with Consumer Key</li>
|
||||
</ul>
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
function WooCommerceConfigNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.baseUrl = (config.baseUrl || "https://shop.hanssonit.se/wp-json/wc/v3").replace(/\/+$/, "");
|
||||
node.consumerKey = config.consumerKey || "";
|
||||
node.consumerSecret = config.consumerSecret || "";
|
||||
|
||||
node.sendRequest = function (method, path, body, msgHeaders, callback) {
|
||||
const url = new URL(node.baseUrl + path);
|
||||
const parsedBody = body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined;
|
||||
|
||||
const headers = Object.assign({ "Accept": "application/json", "Content-Type": "application/json" }, msgHeaders || {});
|
||||
if (parsedBody !== undefined) headers["Content-Length"] = Buffer.byteLength(parsedBody);
|
||||
headers["Authorization"] = "Basic " + Buffer.from(node.consumerKey + ":" + node.consumerSecret).toString("base64");
|
||||
|
||||
const options = {
|
||||
method, hostname: url.hostname,
|
||||
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
||||
path: url.pathname + url.search, headers, rejectUnauthorized: false
|
||||
};
|
||||
|
||||
const req = (url.protocol === "https:" ? https : http).request(options, function (res) {
|
||||
let data = "";
|
||||
res.on("data", chunk => data += chunk);
|
||||
res.on("end", () => {
|
||||
let parsed = null;
|
||||
if (data) { try { parsed = JSON.parse(data); } catch (e) { parsed = data; } }
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
callback(null, { statusCode: res.statusCode, body: parsed, headers: res.headers });
|
||||
} else {
|
||||
const err = new Error("WooCommerce API returned status " + res.statusCode + ": " + data);
|
||||
err.statusCode = res.statusCode;
|
||||
err.body = parsed;
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", callback);
|
||||
if (parsedBody !== undefined) req.write(parsedBody);
|
||||
req.end();
|
||||
};
|
||||
}
|
||||
|
||||
RED.nodes.registerType("woocommerce-config", WooCommerceConfigNode);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("woocommerce-customer", {
|
||||
category: "WooCommerce",
|
||||
color: "#7f54b3",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "woocommerce-config", required: true },
|
||||
operation: { value: "search" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-users",
|
||||
label: function () {
|
||||
return this.name || ("WC Customer " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="woocommerce-customer">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="WooCommerce Customer">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="search">Search by Email</option>
|
||||
<option value="get">Get Customer by ID</option>
|
||||
<option value="create">Create Customer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>Search</b>: msg.email – exact match on customer email<br>
|
||||
<b>Get</b>: msg.customerId – WooCommerce user ID<br>
|
||||
<b>Create</b>: msg.email, msg.firstname, msg.lastname, msg.billing, msg.shipping
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="woocommerce-customer">
|
||||
<p>WooCommerce customer operations: search (by email), get, create.</p>
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function WooCommerceCustomerNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) {
|
||||
node.error("No woocommerce-config node configured");
|
||||
return;
|
||||
}
|
||||
|
||||
node.operation = config.operation || "search";
|
||||
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) {
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
if (done) done(err);
|
||||
else node.error(err, msg);
|
||||
return;
|
||||
}
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.woocommerce = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
|
||||
switch (op) {
|
||||
case "search": {
|
||||
const params = new URLSearchParams();
|
||||
if (msg.email) params.append("email", msg.email);
|
||||
if (msg.limit) params.append("per_page", msg.limit);
|
||||
if (msg.page) params.append("page", msg.page);
|
||||
const qs = params.toString();
|
||||
doRequest("GET", "/customers" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.customerId || msg.payload;
|
||||
if (!id) { node.error("Missing customerId"); if (done) done(new Error("Missing customerId")); return; }
|
||||
doRequest("GET", "/customers/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "create": {
|
||||
const body = msg.payload && typeof msg.payload === "object" ? msg.payload : {
|
||||
email: msg.email || "",
|
||||
first_name: msg.firstname || msg.firstName || "",
|
||||
last_name: msg.lastname || msg.lastName || "",
|
||||
username: msg.username || "",
|
||||
billing: msg.billing || {},
|
||||
shipping: msg.shipping || {}
|
||||
};
|
||||
doRequest("POST", "/customers", body, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("woocommerce-customer", WooCommerceCustomerNode);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("woocommerce-order", {
|
||||
category: "WooCommerce",
|
||||
color: "#7f54b3",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "woocommerce-config", required: true },
|
||||
operation: { value: "list" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-shopping-cart",
|
||||
label: function () {
|
||||
return this.name || ("WC Order " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="woocommerce-order">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="WooCommerce Order">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="list">List Orders</option>
|
||||
<option value="get">Get Order</option>
|
||||
<option value="updateStatus">Update Status</option>
|
||||
<option value="addNote">Add Note</option>
|
||||
<option value="getNotes">Get Notes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>List filters</b>: msg.status (processing, completed, on-hold, etc.),
|
||||
msg.after (ISO 8601 date), msg.before, msg.customer (ID), msg.product (ID),
|
||||
msg.limit (per_page), msg.page<br>
|
||||
<b>Update Status</b>: msg.orderId + msg.status<br>
|
||||
<b>Add Note</b>: msg.orderId + msg.note (+ optional msg.customerNote=true)
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="woocommerce-order">
|
||||
<p>WooCommerce order operations: list, get, updateStatus, addNote, getNotes.</p>
|
||||
<h3>Operations</h3>
|
||||
<ul>
|
||||
<li><b>list</b> – List orders with filters (status, after, before, customer, product)</li>
|
||||
<li><b>get</b> – Get single order by msg.orderId</li>
|
||||
<li><b>updateStatus</b> – Update order status (msg.orderId + msg.status)</li>
|
||||
<li><b>addNote</b> – Add order note (msg.orderId + msg.note)</li>
|
||||
<li><b>getNotes</b> – Get order notes (msg.orderId)</li>
|
||||
</ul>
|
||||
<h3>Order Object Fields</h3>
|
||||
<p><code>billing</code> (first_name, last_name, email, company, address, phone),
|
||||
<code>line_items</code>, <code>total</code>, <code>currency</code>,
|
||||
<code>number</code>, <code>date_created</code>, <code>meta_data</code></p>
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function WooCommerceOrderNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) { node.error("No woocommerce-config node configured"); return; }
|
||||
|
||||
node.operation = config.operation || "list";
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) { node.status({ fill: "red", shape: "ring", text: "error" }); done ? done(err) : node.error(err, msg); return; }
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.woocommerce = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function buildListParams(msg) {
|
||||
const p = new URLSearchParams();
|
||||
if (msg.status) p.append("status", msg.status);
|
||||
if (msg.after) p.append("after", msg.after);
|
||||
if (msg.before) p.append("before", msg.before);
|
||||
if (msg.customer) p.append("customer", msg.customer);
|
||||
if (msg.product) p.append("product", msg.product);
|
||||
if (msg.limit) p.append("per_page", msg.limit);
|
||||
if (msg.page) p.append("page", msg.page);
|
||||
if (msg.order) p.append("order", msg.order);
|
||||
if (msg.orderby) p.append("orderby", msg.orderby);
|
||||
return p.toString() ? "?" + p.toString() : "";
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
switch (op) {
|
||||
case "list": {
|
||||
doRequest("GET", "/orders" + buildListParams(msg), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.orderId || msg.payload;
|
||||
if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; }
|
||||
doRequest("GET", "/orders/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "updateStatus": {
|
||||
const id = msg.orderId || (msg.payload && msg.payload.id);
|
||||
const status = msg.status || msg.newStatus;
|
||||
if (!id || !status) { node.error("Missing orderId or status"); done && done(new Error("Missing orderId or status")); return; }
|
||||
doRequest("PUT", "/orders/" + encodeURIComponent(id), { status }, msg, done);
|
||||
break;
|
||||
}
|
||||
case "addNote": {
|
||||
const id = msg.orderId || (msg.payload && msg.payload.id);
|
||||
const note = msg.note || msg.noteText || (msg.payload && msg.payload.note);
|
||||
if (!id || !note) { node.error("Missing orderId or note"); done && done(new Error("Missing orderId or note")); return; }
|
||||
doRequest("POST", "/orders/" + encodeURIComponent(id) + "/notes", { note, customer_note: msg.customerNote || false }, msg, done);
|
||||
break;
|
||||
}
|
||||
case "getNotes": {
|
||||
const id = msg.orderId || (msg.payload && msg.payload.id);
|
||||
if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; }
|
||||
doRequest("GET", "/orders/" + encodeURIComponent(id) + "/notes", undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("woocommerce-order", WooCommerceOrderNode);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("woocommerce-product", {
|
||||
category: "WooCommerce",
|
||||
color: "#7f54b3",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "woocommerce-config", required: true },
|
||||
operation: { value: "list" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-cube",
|
||||
label: function () {
|
||||
return this.name || ("WC Product " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="woocommerce-product">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="WooCommerce Product">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="list">List Products</option>
|
||||
<option value="get">Get Product by ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>List</b>: msg.limit, msg.page, msg.category (ID), msg.search<br>
|
||||
<b>Get</b>: msg.productId – WooCommerce product ID<br>
|
||||
Useful for enriching order data with product details.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="woocommerce-product">
|
||||
<p>WooCommerce product operations: list, get.</p>
|
||||
<p>Useful for enriching order line_items with product details (name, SKU, image).</p>
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function WooCommerceProductNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) {
|
||||
node.error("No woocommerce-config node configured");
|
||||
return;
|
||||
}
|
||||
|
||||
node.operation = config.operation || "list";
|
||||
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) {
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
if (done) done(err);
|
||||
else node.error(err, msg);
|
||||
return;
|
||||
}
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.woocommerce = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
|
||||
switch (op) {
|
||||
case "list": {
|
||||
const params = new URLSearchParams();
|
||||
if (msg.limit) params.append("per_page", msg.limit);
|
||||
if (msg.page) params.append("page", msg.page);
|
||||
if (msg.category) params.append("category", msg.category);
|
||||
if (msg.search) params.append("search", msg.search);
|
||||
const qs = params.toString();
|
||||
doRequest("GET", "/products" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.productId || msg.payload;
|
||||
if (!id) { node.error("Missing productId"); if (done) done(new Error("Missing productId")); return; }
|
||||
doRequest("GET", "/products/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("woocommerce-product", WooCommerceProductNode);
|
||||
};
|
||||
Reference in New Issue
Block a user