Fixing Sort Order when Cloning Category Trees - data-access-layer

I am working on a FOSS plugin to clone categories including sub categories in Shopware 6 (as suggested in NEXT-11360).
It's mostly based on this code from the German forum.
async duplicateElement(contextItem) {
const behavior = {
cloneChildren: true,
overwrites: {
name: `${contextItem.data.name} ${this.$tc('global.default.copy')}`,
path: null,
afterCategoryId: null,
updatedAt: null,
}
};
await this.categoryRepository.clone(contextItem.id, Shopware.Context.api, behavior).then((clone) => {
const criteria = new Criteria();
criteria.setIds([clone.id]);
this.categoryRepository.search(criteria).then((categories) => {
this.addCategories(categories);
});
}).catch(() => {
this.createNotificationError({
message: this.$tc('global.notification.unspecifiedSaveErrorMessage'),
});
});
},
This works suprisingly well at the first glance.
The current problem is, that the afterCategoryId is just copied, then pointing to the source categories and thus sort order is not maintained.
I tried to set this null in the overwrites, but this is not working recursively. Also I tried to set it to null as a proof of concept in the VersionManager
foreach(array_keys($data['children']) as $key) {
unset($data['children'][$key]['afterCategoryId']);
}
but then there is no sort order at all :-)
My next approach would be to subscribe to
\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent
and fix the sort order after the entities have been copied.
I believe there is no suitable even to fix up the data before it is persisted?
Or is there an elegant way?

While it is convenient to be able to clone the complete tree with the root, I think it will probably less of a headache to implement your own clone endpoint, if you want to maintain the order of categories within a branch.
Here's a quick, untested implementation of an endpoint:
/**
* #Route("/api/_admin/my-plugin/clone-category/{categoryId}", name="api.admin.my-plugin.clone-category", methods={"GET"}, defaults={"_routeScope"={"administration"}})
*/
public function cloneCategory(string $categoryId, Request $request, Context $context): JsonResponse
{
$newId = Uuid::randomHex();
$cloneBehavior = new CloneBehavior([
'afterCategoryId' => null,
], false);
$this->categoryRepository->clone($categoryId, $context, $newId, $cloneBehavior);
$this->cloneChildren($categoryId, $newId, $context);
return new JsonResponse($newId);
}
private function cloneChildren(string $parentId, string $newParentId, Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('parentId', $parentId));
/** #var CategoryCollection $collection */
$collection = $this->categoryRepository->search($criteria, $context)->getEntities();
if ($collection->count() === 0) {
return;
}
$children = $collection->sortByPosition();
$previousId = null;
foreach ($children as $child) {
$cloneBehavior = new CloneBehavior([
'parentId' => $newParentId,
'afterCategoryId' => $previousId,
], false);
$newId = Uuid::randomHex();
$this->categoryRepository->clone($child->getId(), $context, $newId, $cloneBehavior);
$this->cloneChildren($child->getId(), $newId, $context);
$previousId = $newId;
}
}

Related

Whatsapp Web - how to access data now?

It used to be possible to access http://web.whatsapp.com/ with the Store object in JavaScript. A few hours ago, this stopped working. How does it update chat data now? It must save the data somewhere.
I'm using this to get the Store again:
setTimeout(function() {
// Returns promise that resolves to all installed modules
function getAllModules() {
return new Promise((resolve) => {
const id = _.uniqueId("fakeModule_");
window["webpackJsonp"](
[],
{
[id]: function(module, exports, __webpack_require__) {
resolve(__webpack_require__.c);
}
},
[id]
);
});
}
var modules = getAllModules()._value;
// Automatically locate modules
for (var key in modules) {
if (modules[key].exports) {
if (modules[key].exports.default) {
if (modules[key].exports.default.Wap) {
store_id = modules[key].id.replace(/"/g, '"');
}
}
}
}
}, 5000);
function _requireById(id) {
return webpackJsonp([], null, [id]);
}
// Module IDs
var store_id = 0;
var Store = {};
function init() {
Store = _requireById(store_id).default;
console.log("Store is ready" + Store);
}
setTimeout(function() {
init();
}, 7000);
Just copy&paste on the console and wait for the message "Store is ready".
Enjoy!
To explain Pablo's answer in detail, initially we load all the Webpack modules using code based on this How do I require() from the console using webpack?.
Essentially, the getAllModules() returns a promise with all the installed modules in Webpack. Each module can be required by ID using the _requireById(id) which uses the webpackJsonp(...) function that is exposed by Webpack.
Once the modules are loaded, we need to identify which id corresponds to the Store. We search for a module containing exports.default.Wap and assign it's id as the Store ID.
You can find more details on my github wiki here
A faster method:
I grab the source of the "app" and find the store object then
I save it in ZStore global variable. :D
!function(){for(var t of document.getElementsByTagName("script"))t.src.indexOf("/app.")>0&&fetch(t.src,{method:"get"}).then(function(t){return t.text().then(function(t){var e=t.indexOf('var a={};t["default"]')-89;window.ZStore=window.webpackJsonp([],null,JSON.stringify(t.substr(e,10))).default})})}();
window.ZStore will contain the object.
Non minified version:
(function() {
function getStore(url) {
fetch(url, {
"method": 'get'
}).then(function(response) {
return response.text().then(function(data) {
var offset = data.indexOf('var a={};t["default"]') - 89;
window.ZStore = window.webpackJsonp([], null, JSON.stringify(data.substr(offset, 10))).default
});
});
}
for (var e of document.getElementsByTagName("script")) {
if (e.src.indexOf("/app.") > 0) getStore(e.src);
}
})();

FlatList single select cell

I followed the example from official docs, here is how to implement multiselection feature:
state = { selected: (new Map(): Map<string, boolean>) };
onPressItem = (id) => {
this.setState((state) => {
const selected = new Map(state.selected);
selected.set(id, !selected.get(id));
return { selected };
});
};
I'm struggling with making it single select though. It's easy to return new Map with false values anytime cell is tapped, but that means the cell cannot be deselected by another tap on it, which is the desired feature in my case.
onPressItem = (id) => {
this.setState((state) => {
const selected = new Map();
selected.set(id, !selected.get(id));
return { selected };
});
};
How would you implement it? Should I use lodash to iterate over the Map to find the one that already is true and change its value (now sure how to iterate over Map though), or maybe there is some better approach I am missing right now?
EDIT
Iterating over elements of the selected Map seems to be a really ugly idea, but it is simple and it actually works. Is there any better way to do it that I am missing out on?
onPressItem = (id: string) => {
this.setState((state) => {
const selected = new Map(state.selected);
selected.set(id, !selected.get(id));
for (const key of selected.keys()) {
if (key !== id) {
selected.set(key, false);
}
}
return { selected };
});
};
Thanks in advance
You can just set only one value instead of a map like this
onPressItem = (id) => {
this.setState((state) => {
const selected = selected === id ? null : id;
return { selected };
});
};
I had the same issue, my solution was:
_onPressItem = (id: string) => {
// updater functions are preferred for transactional updates
this.setState((state) => {
// copy the map rather than modifying state.
const selected = new Map(state.selected);
// save selected value
let isSelected = selected.get(id);
// reset all to false
selected.forEach((value, key) => {
selected.set(key, false);
});
// then only activate the selected
selected.set(id, !isSelected);
return { selected };
});
};

Using map to reduce in Gun

I am new to Gun. I have existing code that very effectively reduces an array of objects based on a pattern. I am thinking I should tweak this to run in the context of Gun's .map and return undefined for non-matches. I think I will also have to provide two arguments, one of which is the where clause and the other the properties I want shown on returned objects. I also presume that if I use .on future matches will automagically get spit out! Am I on the right path?
const match = (object,key,value) => {
const type = typeof(value);
if(value && type==="object") {
return Object.keys(value).every(childkey =>
match(object[key],childkey,value[childkey]));
if(type==="function") return value(object[key]);
return object[key]===value;
}
const reduce = (objects,where) => {
const keys = Object.keys(where);
return objects.reduce((accumulator,current) => {
if(keys.every(key => match(current,key,where[key]))) {
accumulator.push(current);
}
return accumulator;
},[]);
}
let rows = reduce([{name: "Joe",address:{city: "Seattle"},age:25},
{name: "Mary",address:{city: "Seattle"},age:16},
{name: "Joe",address:{city: "New York"},age:20}],
{name: () => true,
address: {city: "Seattle"},
age: (age) => age > 10});
// results in
[{name: "Joe",address:{city: "Seattle"},age:25},
{name: "Mary",address:{city: "Seattle"},age:16}]
Further exploration of this resulted in the code below, which is stylistically different, but conforms to the immediate responsive nature of Gun. However, it is unclear how to deal with nested objects. The code below only works for primitives.
const match = (object,key,value) => {
const type = typeof(value);
if(!object || typeof(object)!=="object") return false;
if(value && type==="object") {
const child = gun.get(object[key]["#"]);
for(let key in value) {
const value = {};
child.get(key).val(v => value[key] = v,{wait:0});
if(!match(value,key,value[key])) return;
}
}
if(type==="function") return value(object[key]);
return object[key]===value;
}
const gun = Gun(["http://localhost:8080/gun"]),
users = [{name: "Joe",address:{city: "Seattle"},age:25},
{address:{city: "Seattle"},age:25},
{name: "Mary",address:{city: "Seattle"},age:16},
{name: "Joe",address:{city: "New York"},age:20}];
//gun.get("users").map().put(null);
for(let user of users) {
const object = gun.get(user.name).put(user);
gun.get("users").set(object);
}
gun.get("users").map(user => {
const pattern = {name: (value) => value!=null, age: (age) => age > 20}; //, address: {city: "Seattle"}
for(let key in pattern) {
if(!match(user,key,pattern[key])) return;
}
return user;
}).on(data => console.log(data));
Yes. GUN's .map method does more than what it seems.
Say we have var users = gun.get('users'). We can do:
users.map() with no callback acts like a forEach because the default callback is to return the data as-is.
users.map(user => user.age * 2) with a callback, it lets you transform the data like you would expect from a map, except where:
users.map(function(){ return }) if you return undefined, it will filter out that record.
WARNING: As of the current time, .map(transform) function is currently experimental and my have bugs with it. Please try it and report any you find.
Now we can combine it with some other methods, to get some cool behavior:
users.map().on(cb) will get current and future users as they are added to the table, and gets notified for updates on each of those users.
users.map().val(cb) will get current and future users as they are added to the table, but only gets each one once.
users.val().map().on(cb) gets only the current users (not future), but gets the updates to those users.
users.val().map().val(cb) gets only the current users (not future), and only gets them once.
So yes, you are on the right track. For instance, I have a test in gun core that does this:
list.map(user => user.age === 27? user.name + "thezombie" : u).on(function(data){
// verify
});
list.set({name: 'alice', age: 27});
list.set({name: 'bob', age: 27});
list.set({name: 'carl', age: 29});
list.set({name: 'dave', age: 25});
This creates a live map that filters the results and locally (view only) transforms the data.
In the future, this is how the SQL and MongoDB Mango query extensions will work for gun.
Note: GUN only loads the property you request on an object/node, so it is bandwidth efficient. If we do users.map().get('age') it will only load the age value on every user, nothing else.
So internally, you can do some efficient checks, and if all your conditionals match, only /then/ load the entire object. Additionally, there are two other options: (1) you can use an in-memory version of gun to create server-side request-response patterns, so you can have server-side filtering/querying that is efficient. (2) if you become an adapter developer and learn the simple wire spec and then write your own custom query language extensions!
Anything else? Hit me up! More than happy to answer.
Edit: My reply in the comments, comments apparently can't have code. Here is pseudo-code of how to "build up" more complex queries, which will be similar to how SQL/Mango query extensions will work:
mutli-value & nested value matching can be "built up" from this as the base, but yes, you are right, until we have SQL/Mango query examples, there isn't a simple/immediate "out of the box" example. This is pseudo code, but should get the idea across:
```
Gun.chain.match = function(query, cb){
var gun = this;
var fields = Object.keys(query);
var check = {};
fields.forEach(function(field){
check[field] = true;
gun.get(field).val(function(val){
if(val !== query[field]){ return }
check[field] = false;
//all checks done?
cb(results)
});
});
return gun;
}
```
Solution, the trick is to use map and not val:
Gun.chain.match = function(pattern,cb) {
let node = this,
passed = true,
keys = Object.keys(pattern);
keys.every(key => {
const test = pattern[key],
type = typeof(test);
if(test && type==="object") {
node.get(key).match(test);
} else if(type==="function") {
node.get(key).map(value => {
if(test(value[key])) {
return value;
} else {
passed = false;
}
});
} else {
node.get(key).map(value => {
if(value[key]===test) {
return value;
} else {
passed = false;
}
});
}
return passed;
});
if(passed && cb) this.val(value => cb(value))
return this;
}
const gun = new Gun();
gun.get("Joe").put({name:"Joe",address:{city:"Seattle"},age:20});
gun.get("Joe").match({age: value => value > 15,address:{ city: "Seattle"}},value => console.log("cb1",value));

Is my order complete return approach correct?

When a customer is returned to the following URL (example);
http://prestashop.dev/index.php?action=completed&controller=callback&fc=module&hmac={valid-hmac}&merchant_order_id=14&module=chippin
After a successful payment, It will call on this FrontController sub-class;
class ChippinCallbackModuleFrontController extends ModuleFrontController
{
public function postProcess()
{
$chippin = new Chippin();
$payment_response = new PaymentResponse();
$payment_response->getPostData();
// if a valid response from gateway
if(ChippinValidator::isValidHmac($payment_response)) {
// "action" is passed as a param in the URL. don't worry, the Hmac can tell if it's valid or not.
if ($payment_response->getAction() === "completed") {
// payment_response->getMerchantOrderId() will just return the id_order from the orders table
$order_id = Order::getOrderByCartId((int) ($payment_response->getMerchantOrderId()));
$order = new Order($order_id);
// this will update the order status for the benefit of the merchant.
$order->setCurrentState(Configuration::get('CP_OS_PAYMENT_COMPLETED'));
// assign variables to smarty (copied this from another gateway, don't really understand smarty)
$this->context->smarty->assign(
array(
'order' => $order->reference,
)
);
// display this template
$this->setTemplate('confirmation.tpl');
I'm quite new to Prestashop. I'm just not sure if this is technically done or not. The confirmation.tlp view does display with the order->reference and the order status is updated to "Completed" but is this all I need?
Are there any other considerations? I have the opportunity to call a hookDisplayPaymentReturn at this point but why should I?
I seem to have a pretty standard return page. Is this enough;
Update - Do I just call a hook something like;
public function displayPaymentReturn()
{
$params = $this->displayHook();
if ($params && is_array($params)) {
return Hook::exec('displayPaymentReturn', $params, (int) $this->module->id);
}
return false;
}
As far as I can see everything seems okay for me.
You should consider adding hookDisplayPaymentReturn it allows other modules to add code to your confirmation page. For example a Google module could add javascript code that sends order informations to analytics on confirmation page.
EDIT
class ChippinCallbackModuleFrontController extends ModuleFrontController
{
public function postProcess()
{
$chippin = new Chippin();
$payment_response = new PaymentResponse();
$payment_response->getPostData();
// if a valid response from gateway
if(ChippinValidator::isValidHmac($payment_response)) {
// "action" is passed as a param in the URL. don't worry, the Hmac can tell if it's valid or not.
if ($payment_response->getAction() === "completed") {
// payment_response->getMerchantOrderId() will just return the id_order from the orders table
$order_id = Order::getOrderByCartId((int) ($payment_response->getMerchantOrderId()));
$order = new Order($order_id);
// this will update the order status for the benefit of the merchant.
$order->setCurrentState(Configuration::get('CP_OS_PAYMENT_COMPLETED'));
// assign variables to smarty (copied this from another gateway, don't really understand smarty)
$this->context->smarty->assign(
array(
'order' => $order->reference,
'hookDisplayPaymentReturn' => Hook::exec('displayPaymentReturn', $params, (int) $this->module->id);
)
);
$cart = $this->context->cart;
$customer = new Customer($cart->id_customer);
Tools::redirect('index.php?controller=order-confirmation&id_cart='.$cart->id.'&id_module='.$this->module->id.'&id_order='.$order->id.'&key='.$customer->secure_key);
And in your module :
class myPaymentModule extends PaymentModule
{
public function install()
{
if (!parent::install() || !$this->registerHook('paymentReturn'))
return false;
return true;
}
// Example taken from bankwire module
public function hookPaymentReturn($params)
{
$state = $params['objOrder']->getCurrentState();
$this->smarty->assign(array(
'total_to_pay' => Tools::displayPrice($params['total_to_pay'], $params['currencyObj'], false),
'bankwireDetails' => Tools::nl2br($this->details),
'bankwireAddress' => Tools::nl2br($this->address),
'bankwireOwner' => $this->owner,
'status' => 'ok',
'id_order' => $params['objOrder']->id
));
if (isset($params['objOrder']->reference) && !empty($params['objOrder']->reference))
$this->smarty->assign('reference', $params['objOrder']->reference);
return $this->display(__FILE__, 'confirmation.tpl');
}
}

Express JS what is the correct way to inject non-field property into Mongoose model before return it to client

Using Mongoose model method findOne, I receive a model. Now I want to custom that model before sending it to client, augmenting several attributes into that model.
However, the only way I found for the moment is to turn that model into plain object and augment that object.
I don't know is there any better way doing it?
Here is my lengthy code for that simple purpose:
Topic.find({}).exec(function (err, topics) {
var i, topic_obj, topic_obj_list;
topic_obj_list = [];
if (err) { return next(err); }
for (i = 0; i < topics.length; i++) {
topic_obj = topics[i].toObject();
if (req.user.is_following) {
topic_obj.is_following = true;
} else {
topic_obj.is_following = false;
}
topic_obj_list.push(topic_obj);
}
return res.json(200, topic_obj_list);
});
P/S: I already tried simple solution like: topics[i].is_following = true, bit it didn't work.
You can shorten it to something like this:
Topic.find({}).exec(function (err, topics) {
if (err) { return next(err); }
return res.json(topics.map(function(topic) {
return topic.set(
'is_following',
req.user.is_following ? true : false,
{ strict : false }
);
}));
});
Explanation:
topics.map runs a function on each item of the topics array; the value that is returned from the function ends up in the result returned by map;
with topic.set(FIELD, VALUE, [{ strict : false }]) you can add/overwrite fields of a Mongoose document; when strict is false, the field doesn't have to exist in the schema;