How to clone a Model in mobx? - mobx

I have a simple Model with observable id id
class TodoStore {
todos = [];
id: generateId();
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
}
const todoStore = new TodoStore();
now I want to add a feature to clone this todo just by duplicate this exact model with new generated id with it's todos and any extra infos.
How to do that? .toJS and .createViewModel doesn't work!

Related

Mikro-Orm - ManyToMany Relationship how can I delete reference in PivotTable?

I'm using NestJS with mikro-Orm and have a weird behaviour on all of my manyToMany relations.
#ObjectType()
#Entity()
export class Realty {
#Field(() => ID)
#PrimaryKey({ columnType: "uuid" })
id: string = v4();
#Field(() => [Contact])
#ManyToMany(() => Contact, (contact) => contact.realties)
contacts: Collection<Contact>;
}
#ObjectType()
#Entity()
export class Contact {
#Field(() => ID)
#PrimaryKey({ columnType: "uuid" })
id: string = v4();
#Field(() => [Realty])
#ManyToMany(() => Realty, (realty) => realty.contacts, { owner: true })
realties: Collection<Realty>;
}
When I want to delete a realtyReference from a contact, that works fine and the row from the Contact_Realty PivotTable gets removed. But when I try to delete a contactReference from a realty, nothing happens. Does that only work on the owning side?
ContactsService (works):
async update(updateContactInput: UpdateContactInput) {
const { id, realtyIds } = updateContactInput;
const contact = await this.findOneOrFail(id);
const updated = this.contactsRepository.assign(contact, {
realties: await this.realtiesService.find(realtyIds),
});
await this.contactsRepository.persistAndFlush(updated);
return updated;
}
RealtiesService (returns correct updated entity but doesnt remove row in PivotTable):
async update(updateRealtyGeneralInput: UpdateRealtyGeneralInput) {
const { id, contactIds } = updateRealtyGeneralInput;
const realty = await this.realtiesService.findOneOrFail(id);
const updated = this.realtiesRepository.assign(realty, {
contacts: await this.contactsService.find(contactIds),
});
await this.realtiesRepository.persistAndFlush(updated);
return updated;
}
Both return the correct updated entity but only the ContactsService actually removes the row in the pivotTable.
Would really appreciate some help, thanks alot!
I want to remove one or more contacts from a realty and cannot get it to work. Am I doing something wrong?
You always need to have the owning side of your M:N collection initialized/populated, which you apparently don't in the second example. In your case it is Contact.realties, so if you want to manipulate this collection from the inverse side, all the entities you add/remove from the inverse need to have the owning side populated. Only owning side is what is taken into account when computing changesets. I will need to revisit this a bit, we might be able to improve on this thanks to the recent changes like the reference updates added in v5.5.
Also, there is some misunderstanding in your code. assign mutates the parameter, it does not return "modified entity", it mutates the one you pass in the first argument. If that entity is already managed (as in your case), there is no point in re-persisting it again, just flush.
async update(updateRealtyGeneralInput: UpdateRealtyGeneralInput) {
const { id, contactIds } = updateRealtyGeneralInput;
const realty = await this.em.findOneOrFail(Realty, id);
this.realtiesRepository.assign(realty, {
contacts: await this.em.find(Contact, contactIds, { populate: ['realties'] }),
});
await this.em.flush(updated);
return realty;
}

Fixing Sort Order when Cloning Category Trees

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;
}
}

Method to check if item is saved within the Nuxt Store

I currently have a Store that has the "Saved" items from a feed for a user. I'm trying to figure out the best/efficient way to check if the item is already saved within the store.
I can't think of any other way than grabbing the entire store's contents in each feed item and checking whether the id exists? Surely there's a better way?
FeedItem.vue
methods: {
savePost(Post) {
this.$store.commit('savedPosts/addItem', Post)
},
deletePost(Post) {
this.$store.commit('savedPosts/removeItem', Post)
}
}
Store
export const state = () => ({
items: [
],
})
export const mutations = {
updateItemsOnLoad(state, array) {
var oldItems = state.items
var newItems = array.flat()
var joinedItems = newItems.concat(oldItems);
state.items = joinedItems.flat()
},
addItem(state, item) {
state.items.push(item)
this.$warehouse.set('savedPosts', state.items)
},
removeItem(state, item) {
var index = state.items.findIndex(c => c.id == item.id);
state.items.splice(index, 1);
this.$warehouse.set('savedPosts', state.items)
},
}
So my main question: Is there a more efficient way to check whether a post exists within the items array without querying it on every feed item?

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 };
});
};

MobX - Observable value promised in a store constructor using fromPromise stays null when accessed in another store?

So I have 2 stores, an AuthorStore:
class AuthorStore {
constructor() {
// has author.name and is always present in storage
AsyncStorage.getItem('author').then(action((data) => {
this.author = JSON.parse(data);
}));
}
#observable author = null;
}
and a BookStore:
import AuthorStore from 'authorStore';
class BookStore {
#observable book = {
authorName: AuthorStore.author.name,
bookTitle: null
}
}
I keep getting an error in BookStore that it cannot get property of null, as if the AuthorStore.author.name is null. So it's reading the default author value from the AuthorStore without the constructor running first to assign it the value.
I came across the new mobx-utils fromPromise which I think would get the author value if it exists in local storage, and wait for AsyncStorage to assign it to the author observable, so it can be called from another store without being null.
I tried using fromPromise first in the AuthorStore to log the author value, but it shows as Got undefined in console, and the usual null error in the BookStore when it comes to the AuthorStore.author part.
UPDATE:
class AuthorStore {
#observable author = null;
#computed get theAuthor() {
authorPromise = fromPromise(AsyncStorage.getItem('author').then(data => JSON.parse(data)));
// combine with when..
when(
() => authorPromise.state !== "pending",
() => {
console.log("Got Author", authorPromise.reason || authorPromise.value) // This runs, and returns author
console.log("Got Name", authorPromise.reason || authorPromise.value.name) // This runs, and returns name
return authorPromise.value; // This doesn't get returned in BookStore when calling this computed
}
);
}
}
class BookStore {
#observable book = {
authorName: AuthorStore.theAuthor.name, // doesn't get computed returned value from promise
bookTitle: null
}
}
How can I get the fromPromise value assigned by the AuthorStore computed function theAuthor to return the promised authorPromise value into BookStore under authorName?
FromPromise creates a new object wrapping the original promise. So your authorFromStorage is just a normal promise in your example, not having a state property at all. So you should change your code to:
authorPromise = fromPromise(AsyncStorage.getItem('author').then(data => JSON.parse(data)))
And then when(() => authorPromise.state !== "pending") etc..
** UPDATE **
class AuthorStore {
#observable author = null;
constructor() {
AsyncStorage.getItem('author').then(data => { this.author = JSON.parse(data) });
}
}
class BookStore {
#observable book = {
authorName: function() { // a function in an observable creates a computed prop
return AuthorStore.author && AuthorStore.author.name
},
bookTitle: null
}
}