Reordering Array for Todos in MST Mobx State Tree - mobx

I would like to reorder arrays when using mobx state tree.
Say I have this example taken from the example page.
How do I get to reorder my ToDos in the TodoStore.
As a simplified example, say my todos are ['todo1, todo2'], how do I change them so that the new array is ['todo2, todo1']?
const Todo = types
.model({
text: types.string,
completed: false,
id: types.identifierNumber
})
.actions((self) => ({
remove() {
getRoot(self).removeTodo(self)
},
edit(text) {
if (!text.length) self.remove()
else self.text = text
},
toggle() {
self.completed = !self.completed
}
}))
const TodoStore = types
.model({
todos: types.array(Todo),
filter: types.optional(filterType, SHOW_ALL)
})
.views((self) => ({
get completedCount() {
return self.todos.filter((todo) => todo.completed).length
},
}))
.actions((self) => ({
addTodo(text) {
const id = self.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
self.todos.unshift({ id, text })
},
removeTodo(todo) {
destroy(todo)
},
}))
export default TodoStore
Thanks a lot!

If you want move the second todo to the first index in the array you could create a new action and splice the second todo out and then unshift it back in:
swapFirstTwoTodos() {
const secondTodo = self.todos.splice(1, 1)[0];
self.todos.unshift(secondTodo);
}

Related

this.state return empty array when render

I wanted to display the array series and when I tried to console the array this.state.series in the function it does has the result and the value inside but when I render this.state.series just keep giving empty array. I wonder is it because of the componentDidMount()?
constructor(props){
super(props);
this.state={series:[]}
}
GetTransaction=()=> {
var APIURL="http://10.0.2.2:80/api/getTransaction.php";
var headers={
'Accept':'application/json',
'Content-Type':'application.json'
}
fetch(APIURL,
{
method:'POST',
headers: headers,
})
.then((response)=>response.json())
.then((response)=>
{
this.setState({results:response});
this.state.results[0].map((a) => {
this.state.series.push(a.count)
});
console.log(this.state.series)
})
.catch((error)=>
{
alert("Error"+error);
}
)
}
componentDidMount(){ // runs after the component output has been rendered to the DOM
this.GetTransaction();
}
render(){
console.log(this.state.series)
output
Array []
Array []
Array []
Array []
Array []
Array []
Array []
Array [
"1",
"2",
"1",
"1",
]
There are basically 2 errors in GetTransaction state assignment:
you can't read a state just assigned because this.setState is async. If you want to get the very last value of state you should use this.setState's callback:
this.setState({results:response}, () => {
console.log(this.state.result); //<-- here you have the very last value of result
});
state must be always setted with this.setState: this.state.series.push(a.count) is not good.
So you have to rewrite your code in this way:
...
this.setState({results:response}, () => {
let seriesAppo = [];
this.state.results[0].map((a) => {
seriesAppo.push(a.count);
});
this.setState({series: seriesAppo}, () => {
console.log(this.state.series);
})
});
...
That‘s weird
this.state.results[0].map((a) => {
this.state.series.push(a.count)
});
Don‘t manipulate state that way, only with setState.
const series = response[0];
this.setState({series: series});
Even if you wanna add elements to an array you have to recreate the array. You can achieve this as follows:
const series = response[0];
this.setState({series: […this.state.series, …series]});

getters not reactive in vuex

I have following store defined:
state: () => ({
infoPackCreationData: null,
infoPackCreationTab: null,
}),
getters: {
infoPackImage(state: any) {
return state.infoPackCreationTab && state.infoPackCreationTab.infopackContents
? state.infoPackCreationTab.infopackContents.filter((item: any) => item.type === "IMAGE")
: [];
}
},
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents.filter((item: any) => {if(item.type === "IMAGE")
item = infopackImageData
console.log(item , 'this is items');
return item})
}
},
actions: {
setImageData(context: any, payload: any) {
context.commit('setImageData', payload)
}
}
and in my component I am using the computed to get the imageList:
computed: {
...mapGetters("creationStore", ["infoPackImage"]),
imageList: {
get() {
return this.infoPackImage ?? [];
},
set(value) {
this.$store.dispatch('creationStore/setImageData', value);
}
}
},
The problem is I want to edit a value of the imageList by index using draggable libarary,
but imageList does not act reactive and it just move the image and not showing the other image in the previous index:
async imageChange(e) {
this.loading = true
let newIndex = e.moved.newIndex;
let prevOrder = this.imageList[newIndex - 1]?.order ?? 0
let nextOrder = this.imageList[newIndex + 1]?.order ?? 0
const changeImageOrder = new InfopackImageService();
try {
return await changeImageOrder.putImageApi(this.$route.params.infopackId,
this.$route.params.tabId,
e.moved.element.id, {
title: e.moved.element.title,
infopackAssetRef: e.moved.element.infopackAssetRef,
order: nextOrder,
previousOrder: prevOrder,
}).then((res) => {
let image = {}
let infopackAsset = e.moved.element.infopackAsset
image = {...res, infopackAsset};
Vue.set(this.imageList, newIndex , image)
this.loading = false
return this.imageList
});
} catch (e) {
console.log(e, 'this is put error for tab change')
}
},
Array.prototype.filter doesn't modify an array in-place, it returns a new array. So this mutation isn't ever changing any state:
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents.filter((item: any) => {if(item.type === "IMAGE")
item = infopackImageData
console.log(item , 'this is items');
return item})
}
},
So, if you intend to change state.infoPackCreationTab.infopackContents, you'll need to assign the result of filter():
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents = state.infoPackCreationTab.infopackContents.filter(...)
However, since state.infoPackCreationTab did not have an infopackContents property during initialization, it will not be reactive unless you use Vue.set() or just replace the whole infoPackCreationTab object with a new one (see: Vuex on reactive mutations):
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab = {
...state.infoPackCreationTab,
infopackContents: state.infoPackCreationTab.infopackContents.filter(...)
};

Mobx State Tree - Computed Property depending on Child Array Properties

I have the following case
export const AssetList = types
.model({
assets: types.array(Asset),
})
.views((self) => ({
get current() {
console.log('current updated')
return self.assets.find((el) => el.liked === null)
},
}))
Then the Asset model looks like
export const Asset = types
.model({
liked: types.maybeNull(types.boolean),
})
.actions((self) => ({
like() {
console.log('like')
self.liked = true
},
dislike() {
console.log('dislike')
self.liked = false
},
}))
The problem comes when like() is fired, it updates the liked property of the Asset but then the AssetList computed property current does not update. Not sure why this is happening or if I need to add something extra in order to make it work.

Sequelize query with a where clause on an include of an include

I'm struggling to create a query with sequelize.
Some context
I have the following models:
A Manifestation can have [0..n] Event
An Event belongs to one Manifestation (an Event cannot exist without a Manifestation)
A Place can have [0..n] Event
An Event belongs to one Place (an Event cannot exist without a Place)
A Manifestation can have [1..n] Place
A Place can have [0..n] Manifestation
I model the relations as the following:
Manifestation.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Manifestation)
Place.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Place)
Manifestation.belongsToMany(Place, { through: 'manifestation_place' })
Place.belongsToMany(Manifestation, { through: 'manifestation_place' })
For me it seems rather correct, but don't hesitate if you have remarks.
The question
I'm trying to query the Place in order to get all Manifestation and Event happening in a given Place. But for the Event ones, I want to include them within their Manifestation even if the Manifestation doesn't happen in the given Place.
Below is the "JSON" structure I'm trying to achieve:
{
id: 1,
name: "Place Name",
address: "Place address",
latitude: 47.00000,
longitude: -1.540000,
manifestations: [
{
id: 10,
title: "Manifestation one",
placeId: 1,
events: []
},
{
id: 11,
title: "Manifestation two",
placeId: 3,
events: [
id: 5,
title: "3333",
manifestationId: 11,
placeId: 1
]
}
]
}
So I want to include the Manifestation with id: 11, because one of its Event occurs in the given Place (with id: 1)
Update (04/06/20): For now I rely on javascript to get the expected result
I figured out it would be nice if I posted my current solution before asking.
router.get('/test', async (req, res) => {
try {
const placesPromise = place.findAll()
const manifestationsPromise = manifestation.findAll({
include: [
{ model: event },
{
model: place,
attributes: ['id'],
},
],
})
const [places, untransformedManifestations] = await Promise.all([
placesPromise,
manifestationsPromise,
])
const manifestations = untransformedManifestations.map(m => {
const values = m.toJSON()
const places = values.places.map(p => p.id)
return { ...values, places }
})
const result = places
.map(p => {
const values = p.toJSON()
const relatedManifestations = manifestations
.filter(m => {
const eventsPlaceId = m.events.map(e => e.placeId)
return (
m.places.includes(values.id) ||
eventsPlaceId.includes(values.id)
)
})
.map(m => {
const filteredEvents = m.events.filter(
e => e.placeId === values.id
)
return { ...m, events: filteredEvents }
})
return { ...values, manifestations: relatedManifestations }
})
.filter(p => p.manifestations.length)
return res.status(200).json(result)
} catch (err) {
console.log(err)
return res.status(500).send()
}
})
But I'm pretty sure I could do that directly with sequelize. Any ideas or recommendations ?
Thanks
This is not optimum. But you can try it out:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}).then(manifestations => {
const out = Object.assign({}, {
id: place.id,
name: place.name,
address: place.address,
latitude: place.latitude,
longitude: place.longitude,
manifestations: manifestations.reduce((res, manifestation) => {
if (manifestation.placeId === place.id || manifestation.Event.length > 0) {
res.push({
id: manifestation.id,
title: manifestation.id,
placeId: manifestation.placeId,
events: manifestation.Event
})
}
return res;
}, [])
})
})
resolve(out);
})
})
}
From this, you get all manifestations that assigned to place or have any event that assigns. All included events in the manefestations are assigned to the place.
Edit :
You will be able to use the following one too:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
include: [{
model: db.Manefestation,
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}],
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}],
where: {
placeId: {
$not: id
}
}
}).then(manifestations => {
place.Manefestation = place.Manefestation.concat(manifestations.filter(m=>m.Event.length>0))
resolve(place);// or you can rename, reassign keys here
})
})
})
}
Here I take only direct manifestations in the first query. Then, manifestations that not included and concatenate.
I do not know if you figure it out by now. But the solution is provided below.
Search with Sequelize could get funny :). You have to include inside another include. If the query gets slow use separate:true.
Place.findAll({
include: [
{
model: Manifestation,
attributes: ['id'],
include: [{
model: Event ,
attributes: ['id']
}]
},
],
})
I tried to complete it in a single query but you will still need JavaScript to be able to get the type of output that you want.
(Note: 💡 You need manifestation which is not connected to places but should be included if a event is present of that place. The only SQL way to get that starts by doing a CROSS JOIN between all tables and then filtering out the results which will be a very hefty query)
I came up with this code(tried & executed) which doesn't need you to execute 2 findAll that fetches all data as what you are currently using. Instead it fetched only the data needed for final output in 1 query.
const places = await Place.findAll({
include: [{
model: Manifestation,
// attributes: ['id']
through: {
attributes: [], // this helps not get keys/data of join table
},
}, {
model: Event,
include: [{
model: Manifestation,
// attributes: ['id']
}],
}
],
});
console.log('original output places:', JSON.stringify(places, null, 2));
const result = places.map(p => {
// destructuring to separate out place, manifestation, event object keys
const {
manifestations,
events,
...placeData
} = p.toJSON();
// building modified manifestation with events array
const _manifestations = manifestations.map(m => {
return ({ ...m, events: [] })
});
// going through places->events to push them to respective manifestation events array
// + add manifestation which is not directly associated to place but event is of that manifestation
events.map(e => {
const {
manifestation: e_manifestation, // renaming variable
...eventData
} = e;
const mIndex = _manifestations.findIndex(m1 => m1.id === e.manifestationId)
if (mIndex === -1) { // if manifestation not found add it with the events array
_manifestations.push({ ...e_manifestation, events: [eventData] });
} else { // if found push it into events array
_manifestations[mIndex].events.push(eventData);
}
});
// returning a place object with manifestations array that contains events array
return ({ ...placeData, manifestations: _manifestations });
})
// filter `.filter(p => p.manifestations.length)` as used in your question
console.log('modified places', JSON.stringify(result, null, 2));

Duplicate items in list after an API update

I'm learning vuejs and I'm doing a weather app, the goal is to rank cities with an index (humidex). I fetch weather information by API (axios) in order to collect data from several cities. I want to auto update data every x minutes, problem : some of my results are duplicated (the new data don't replace the old one).
I tried to set an unique key (based on latitude and longitude) for each item, it works for several results but not for all.
data () {
return {
items:[],
show: false,
cities: cities,
newCity:''
}
},
components: {
Item
},
computed: {
sortHumidex() {
return this.items.slice().sort((a,b) => {
return this.getHumidex(b) - this.getHumidex(a) || b.current.temp_c - a.current.temp_c
})
}
},
methods: {
addCity() {
if (this.newCity.trim().length == 0) {
return
}
this.cities.push(this.newCity)
this.newCity = ''
},
getHumidex: (el) => {
const e = 6.112 * Math.pow(10,(7.5*el.current.temp_c/(237.7+el.current.temp_c)))
*(el.current.humidity/100)
return Math.round(el.current.temp_c + 5/9 * (e-10))
},
indexGeo: (e) => {
const lat = Math.round(Math.abs(e.location.lat))
const lon = Math.round(Math.abs(e.location.lon))
return lat.toString() + lon.toString()
},
getApi: function () {
const promises = [];
this.cities.forEach(function(element){
const myUrl = apiUrl+element;
promises.push(axios.get(myUrl))
});
let self = this;
axios
.all(promises)
.then(axios.spread((...responses) => {
responses.forEach(res => self.items.push(res.data))
}))
.catch(error => console.log(error));
}
},
created() {
this.getApi()
this.show = true
}
}
The render when I update API :
By pushing to the existing array of items, you have to deal with the possibility of duplicates. This can be eliminated simply by replacing items every time the API call is made.
Replace:
responses.forEach(res => self.items.push(res.data))
with:
self.items = responses.map(res => res.data)