ReactiveAggregate() + collection.update() -> Error: Expected to find a document to change - javascript

The reactive aggregation is published to the client initially without errors. The error seems to be triggered, when the Meteor.user collection is being updated by a Meteor.call() from the client:
updateProductFavorites = (product_key, action) => {
const { ranking } = this.props
const { product_keys } = ranking[0]
Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else
this.getProductsByKeys(product_keys)
})
}
I have subscribed to both the Meteor.user() and the reactive aggregation:
export default withTracker(() => {
const handle = Meteor.subscribe("products.RankingList")
return {
ranking: AggregatedProductRanking.find({}).fetch(),
user: Meteor.user(),
isLoading: !handle.ready() || !Meteor.user()
}
})(ProductRankingList)
I have declared and imported the clientCollection on both sides, as also suggested in this answer. This is the relevant code on the server side:
const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
// aggregation stages can be seen in the code snippet below
], { clientCollection: "aggregatedProductRanking"})
Meteor.methods({
'Accounts.updateProductFavorites': function(product_key, action) {
allowOrDeny(this.userId)
action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
return Meteor.users.update({_id: this.userId}, action)
}
})
Meteor.publish('products.RankingList', function() {
const callback = () => this.stop()
allowOrDenySubscription(this.userId, callback)
return getProductRankingList(this)
})
What baffles me, is that the update called by Meteor.call('Accounts.updateProductFavorites') is still executed reliably, even with this error being thrown.
So the change to the logged in Meteor.user() flows back to the client and the component rerenders. Only the subscription of the ReactiveAggregate seems to stop working. The following error is thrown and I have to reload the browser to see the changes in the aggregation result. (full stack trace at the bottom)
Uncaught Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
...
// In a certain case the error message is a bit different:
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
...
I am guessing that this update() is called by the ReactiveAggregate() in order to populate the clientCollection. But what am I doing wrong?
For a more complete code sample:
Server side
import { Meteor } from 'meteor/meteor'
import { ReactiveAggregate } from 'meteor/jcbernack:reactive-aggregate';
// Collections
import { AggregatedProductRanking } from '../imports/collections'
const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
{
$match: { productFavorites: {$ne: [] }}
},{
$project: {
_id: 0,
productFavorites: { $concatArrays: "$productFavorites" },
}
},{
$unwind: "$productFavorites"
},{
$facet: {
rankingList: [
{
$group: {
_id: "$productFavorites",
count: { $sum: 1 }
}
},{
$sort: { "count": -1 }
}
],
product_keys: [
{
$group: {
_id: 0,
product_keys: { $addToSet: "$productFavorites" }
}
}
]
}
},{
$unwind: "$product_keys"
},{
$project: {
_id: 0,
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
Meteor.methods({
'Accounts.updateProductFavorites': function(product_key, action) {
allowOrDeny(this.userId)
action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
return Meteor.users.update({_id: this.userId}, action)
},
'Products.getByProductKey': function(productFavorites) {
allowOrDeny(this.userId)
if (productFavorites == undefined)
productFavorites = Meteor.users.findOne({_id: this.userId}, {fields: {productFavorites: 1}}).productFavorites
if (productFavorites.length > 0) {
return Products.find(
{ product_key: {$in: productFavorites }, price_100_g_ml: {$ne: null} },
{ sort: {product_name: 1} }).fetch()
} else
return []
},
})
function allowOrDenySubscription(userId, callback) {
if (!userId) {
callback()
return;
}
}
Meteor.publish(null, function() {
if (!this.userId)
return false
return Meteor.users.find({_id: this.userId}, { fields: {
firstName: 1, lastName: 1,
zip: 1, city: 1, street: 1, houseNumber: 1,
phone: 1, iban: 1, bic: 1,
memberId: 1, membershipFee: 1,
productFavorites: 1
}})
}, { is_auto: true })
Meteor.publish('products.RankingList', function() {
const callback = () => this.stop()
allowOrDenySubscription(this.userId, callback)
return getProductRankingList(this)
})
Client side
import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
// ... more imports
// Collections
import { AggregatedProductRanking } from '../../../imports/collections';
class ProductRankingList extends Component {
constructor() {
super()
this.state = {
products: [],
productDetails: true,
singleProductDetails: 0,
}
}
getProductsByKeys = (product_keys) => {
Meteor.call('Products.getByProductKey', product_keys, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else {
this.setState({products: response})
console.log(response)
}
})
}
updateProductFavorites = (product_key, action) => {
const { ranking } = this.props
const { product_keys } = ranking[0]
console.log(product_keys)
Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else
this.getProductsByKeys(product_keys)
})
}
toggleProductFavorite = (product_key) => {
const { productFavorites } = this.props.user
if (productFavorites.includes(product_key))
this.updateProductFavorites(product_key, 'remove')
else
this.updateProductFavorites(product_key, 'add')
}
mapProductFavorites = () => {
const { products, productDetails, singleProductDetails } = this.state
const { productFavorites } = this.props.user
const { ranking } = this.props
const { rankingList } = ranking[0]
if (products.length == 0)
return <div className="alert alert-primary col-12">No one has favorited any products at the moment, it seems.</div>
products.map((product, i) => {
const { order_number, supplierId } = product
product["count"] = rankingList.find(product => product._id == `${supplierId}_${order_number}`).count
})
products.sort((a, b) => b.count - a.count)
return (
products.map((product, i) => {
if (product.price_100_g_ml) {
var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
}
const { product_name, units, trading_unit, certificate, origin, order_number, supplierId, count } = product
const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'
return (
<div className="col-lg-6" key={i}>
<div key={i} className="product-card">
<div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
{product_name}
{/* <span className="fa-layers fa-fw heart-with-count">
<FontAwesomeIcon icon="heart"/>
<div className="fa-layers-text">{count}</div>
</span> */}
</div>
{productDetails || singleProductDetails == order_number ?
<>
<div className="card-body">
{euro ?
<>
<div className="product-actions">
<button className={`btn btn-light btn-lg product-${isFavorite}`}
onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
<FontAwesomeIcon icon="heart"/>
<span className="ml-2">{count}</span>
</button>
</div>
<div className="price-100-g-ml">
<small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
<big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
</div>
</> : null}
</div>
<div className="card-footer">
<div className="row">
<div className="col-4">{trading_unit}</div>
<div className="col-4 text-center">{certificate}</div>
<div className="col-4 text-right">{origin}</div>
</div>
</div>
</> : null }
</div>
</div>)
})
)
}
componentDidUpdate(prevProps, prevState) {
const { isLoading, ranking } = this.props
if (ranking.length < 1)
return null
if (!isLoading && (prevProps.ranking != ranking)) {
this.getProductsByKeys(ranking[0].product_keys)
}
}
render() {
const { isLoading, ranking } = this.props
if (isLoading || ranking.length < 1)
return null
return(
<div className="row mt-3">
{this.mapProductFavorites()}
</div>
)
}
}
export default withTracker(() => {
const handle = Meteor.subscribe("products.RankingList")
return {
ranking: AggregatedProductRanking.find({}).fetch(),
user: Meteor.user(),
isLoading: !handle.ready() || !Meteor.user()
}
})(ProductRankingList)
Full Stack Trace
// When loading component through history.push()
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
at livedata_connection.js:1192
at Array.forEach (<anonymous>)
at livedata_connection.js:1191
at Array.forEach (<anonymous>)
at Connection._performWrites (livedata_connection.js:1187)
at Connection._flushBufferedWrites (livedata_connection.js:1167)
at meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1234
// After second call of Meteor.call('Accounts.updateProductFavorites')
Uncaught Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
at livedata_connection.js:1192
at Array.forEach (<anonymous>)
at livedata_connection.js:1191
at Array.forEach (<anonymous>)
at Connection._performWrites (livedata_connection.js:1187)
at Connection._flushBufferedWrites (livedata_connection.js:1167)
at Connection._livedata_data (livedata_connection.js:1133)
at Connection.onMessage (livedata_connection.js:1663)

It turned out, that the solution was pretty simple: The reason for the above mentioned errors was a missing _id field in the aggregation result:
...
},{
$project: {
_id: 0, // This will give the error.
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
The docs for ReactiveAggregate() (meteor-reactive-aggregate) state that the _id field can be omitted, as it will be created automatically by ReactiveAggregate(). But even after removing the _id: 0, it didn't work.
What does work is this:
...
},{
$project: {
_id: "whatTheFuckIsGoingOnHere", // Calm down, bro!
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
Wonderfully reactive aggregations, just for a little pain in the a**.
I made a bug report in the github repo

Related

I cannot implement a filter to my array in vue.js

I've been looking for quite a while but being a novice I can't find an answer.
I would like to filter my array with the id of a property I think is the wrong syntax.
Thanks for your help
components
export default {
props: ["user", "recette"],
data() {
return { email: this.$route.params.email };
},
components: {},
methods: {},
computed: {
filteredItems: function () {
return this.recette.filter((recettes) => {
return recettes.cat_recetteId === 1;
});
},
},
};
VIEW
<template>
<div>
<myrecette :recette="recette"/>
<myfooter />
</div>
</template>
<script>
import myrecette from "../components/recette";
import myfooter from "../components/myfooter";
export default {
name: "",
data() {
return {
recette: "",
user: "",
};
},
components: {
myrecette,
myfooter,
},
created: function() {
this.axios.get("http://localhost:3000/recette/all_recette/").then((res) => {
(this.recette = res.data.recette),
this.axios
.get(
"http://localhost:3000/user/rec_user/" + this.$route.params.email
)
.then((res) => {
this.user = res.data.user;
});
});
},
};
</script>
<style scoped></style>
NODE
router.get("/all_recette", (req, res) => {
db.recette
.findAll({
include: { all: true },
})
.then((recette) => {
if (recette) {
res.status(200).json({
recette: recette,
});
} else {
res.json("il n'y a pas de recettes");
}
})
.catch(err => {
res.json(err);
});
});
Here is my complete code as well as my node route.
My error return is
vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in render: "TypeError: this.recette.filter is not a function"
The filter works by keeping items which return true, so if you want all items having a cat_recetteId of 1, you would change it to:
computed: {
filteredItems: function() {
if (!this.recette) return [];
return this.recette.filter((recettes) => {
return recettes.cat_recetteId === 1;
});
},
},
It's also good practice to use an arrow function in most cases inside of a computed.
Your filter callback function should return true or false. You're 1) not returning anything and 2) assigning a value (=) instead of doing a comparison (==/===).
computed: {
filteredItems: function() {
return this.recette.filter(function(recettes) {
return recettes.cat_recetteId === 1;
});
},
},

How can i clear the chat input-box after sending the message(from suggestion) in botframework webchat?

i'm working on a bot application using react js and botframework webchat. The thing is that i want to clear the text input box (from where msgs are sent) after sending the message - which is selected from the suggestion. The Suggestion list(or autocomplete component) is a custom coded one. And What i mean is that if i type "hr" the suggestion list popup will come and if i click on one option from the suggestion, say 'hr portal', it will be sent, but what i wrote ie "hr" remains there in the input field and i want to clear that. And please note that If i type something and send its working fine. The problem is only when i type something and select something from the suggestion. Everything else is fine. How can i clear that. Any help would be really appreciated.
please find the below image for more understanding.
here's my code;
import React from 'react';
import { DirectLine, ConnectionStatus } from 'botframework-directlinejs';
import ReactWebChat from 'botframework-webchat';
import './ChatComponent.css';
var val;
var apiParameters = [];
var currentFocus = -1;
export default class extends React.Component {
constructor(props) {
super(props);
this.state = {
token: '',
conversationId: '',
directLine: {},
view: false,
feedBack: null,
value: '',
popupContent: '',
storeValue: '',
suggestions: [],
suggestionCallback: '',
suggestionTypedText: "",
typingChecking: "false",
};
this.handleTokenGeneration = this.handleTokenGeneration.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSaveFeedback = this.handleSaveFeedback.bind(this);
this.handleSuggestion = this.handleSuggestion.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleSuggestionClick = this.handleSuggestionClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.moveHighlight = this.moveHighlight.bind(this);
this.getSuggestionHtml = this.getSuggestionHtml.bind(this);
}
getSuggestionHtml(suggestion) {
const lowerCaseSuggestion = suggestion.toLowerCase();
return {
__html: lowerCaseSuggestion.includes(this.state.suggestionTypedText) ? lowerCaseSuggestion
.replace(this.state.suggestionTypedText, `<b>${this.state.suggestionTypedText}</b>`) : lowerCaseSuggestion
};
}
handleTokenGeneration = async () => {
console.log("11111");
const response = await fetch(`api/TokenGenerationService/GetToken`);
const data = await response.json();
this.setState({
token: data.categoryObject.token, conversationId:
data.categoryObject.conversationId
});
this.state.directLine = new DirectLine({ token: this.state.token });
this.setState({ view: true });
this.setState({ typingChecking: "true" });
console.log("conversationId");
};
async handleSuggestion(val, store) {
if (val === "") {
this.setState({
suggestions: []
});
}
else {
apiParameters = [];
var valuess = null;
const response = await fetch(`api/TokenGenerationService/GetAzureSearch?myparam1=${val}`);
const data = await response.json();
var values = ["Hello", "yes", "no", "exit", "Welcome", "Thank You", "Approve", "Apply leave", "Reject", "Absence Balance", "Leave Balance", "Upcoming Holidays", "Apply Comp-Off", "Approve Leave", "Raise Incident Tickets", "Project Allocation Info", "Reporting Manager Change", "Reporting Manager Approval", "Approve Isolve Tickets", "My Manager", "My Account Manager", "Generate Salary Certificate", "Isolve Ticket Status", "Internal Job Posting", "My Designation", "My Joining Date", "RM Approval", "RM Change", "Resource Allocation", "ESettlement Approval", "SO Approval", "Cash advance Approval", "Purchase Request Approval", "Referral status", "HR Ticket", "Platinum Support"];
valuess = values.filter(s =>
s.toLocaleLowerCase().startsWith(val.toLowerCase())
);
valuess = valuess.concat(data.az_search);
this.setState({
suggestions: valuess,
suggestionCallback: store,
suggestionTypedText: val.toLowerCase()
});
// alert(data.az_search);
var totCount = data.az_search;
console.log("kkkkkk" + totCount);
}
}
moveHighlight(event, direction) {
event.preventDefault();
const { highlightedIndex, suggestions } = this.state;
if (!suggestions.length) return;
let newIndex = (highlightedIndex + direction + suggestions.length) % suggestions.length;
if (newIndex !== highlightedIndex) {
this.setState({
highlightedIndex: newIndex,
});
}
}
keyDownHandlers = {
ArrowDown(event) {
this.moveHighlight(event, 1);
},
ArrowUp(event) {
this.moveHighlight(event, -1);
},
Enter(event) {
const { suggestions } = this.state;
if (!suggestions.length) {
// menu is closed so there is no selection to accept -> do nothing
return
}
event.preventDefault()
this.applySuggestion(suggestions[this.state.highlightedIndex]);
},
}
handleKeyDown(event) {
// console.log("lokkkkkkkkkkkk")
if (this.keyDownHandlers[event.key])
this.keyDownHandlers[event.key].call(this, event)
}
async handleSuggestionClick(event) {
await this.applySuggestion(event.currentTarget.textContent);
}
async applySuggestion(newValue) {
//newValue = null;
await this.setState({ typingChecking: "false", suggestions: [], highlightedIndex: 0 });
this.state.suggestionCallback.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: newValue
}
});
await this.setState({ typingChecking: "true" });
}
getSuggestionCss(index) {
var HIGHLIGHTED_CSS = "HIGHLIGHTED_CSS";
var SUGGESTION_CSS = "SUGGESTION_CSS";
return index === this.state.highlightedIndex ? HIGHLIGHTED_CSS : SUGGESTION_CSS;
}
handleClose(elmnt) {
var x = document.getElementsByClassName("autocomplete-items");
for (var i = 0; i < x.length; i++) {
if (elmnt !== x[i]) {
x[i].parentNode.removeChild(x[i]);
}
}
}
async componentDidMount() {
try {
await this.handleTokenGeneration();
const store =
window.WebChat.createStore(
{},
({ getState }) => next => action => {
this.state.directLine.connectionStatus$
.subscribe(connectionStatus => {
if (connectionStatus === ConnectionStatus.ExpiredToken) {
console.log("expired");
}
if (action.type === 'WEB_CHAT/SET_SEND_BOX') {
const val = action.payload.text;
if (this.state.typingChecking === "true") {
this.setState({
highlightedIndex: -1,
});
console.log(this.state.typingChecking);
this.handleSuggestion(val, store);
}
}
if (action.type === 'DIRECT_LINE/DISCONNECT_FULFILLED') {
console.log("final" + connectionStatus);
console.log("finalexpired" + ConnectionStatus.ExpiredToken);
console.log("final");
this.handleTokenGeneration();
}
});
return next(action)
}
);
this.setState({ storeValue: store });
} catch (error) {
console.log("error in fetching token");
console.log(error);
}
this.state.directLine.activity$
.filter(activity => activity.type === 'message')
.subscribe(function (activity) {
//console.log("oooooooooooooooooooooo");
}
// message => console.log("received message ", message.text)
);
}
handleSaveFeedback(ans) {
// console.log(this.state.conversationId);
// console.log(this.state.feedBack);
var userID = "C94570";
var feedbackmsg = this.state.value;
var feedbacktype = this.state.feedBack;
var convId = this.state.conversationId;
fetch('api/Feedback/SaveFeedback',
{
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Uid: userID, FeedbackMessage: feedbackmsg, Convid: convId, FeedbackType: feedbacktype })
}).
then(response => response.text())
.then(data => {
console.log(data.getResult);
});
this.setState({ value: '' });
}
feedback(ans) {
this.setState({ feedBack: ans });
if (ans === "Send") {
this.handleSaveFeedback(ans);
}
else if (ans === "Yes") {
this.setState({ popupContent: "How was your experience?" });
// console.log(this.state.value)
}
else if (ans === "No") {
this.setState({ popupContent: "What went wrong?" });
// console.log(this.state.value)
}
}
handleChange = (event) => {
this.setState({ value: event.target.value });
}
styleOptions = {
bubbleBackground: 'rgba(0, 0, 255, .1)',
bubbleFromUserBackground: 'rgba(0, 255, 0, .1)',
botAvatarInitials: 'DIA',
userAvatarInitials: 'ME'
}
render() {
if (!this.state.view) {
return
<div />
} else {
const filteredSuggestions = this.state.suggestions.filter(
suggestion =>
suggestion.toLowerCase().indexOf(this.state.suggestionTypedText.toLowerCase())
> -1
);
// console.log(this.state.view);
return (
<div className="react-container webchat" >
<div onKeyDown={this.handleKeyDown.bind(this)}>
<div >
<ReactWebChat styleOptions={this.styleOptions} directLine={this.state.directLine} webSocket={true} userID='C94570' username='Thomas' store={this.state.storeValue} sendTypingIndicator={true} />
</div>
</div>
<div className="SuggestionParent" id="Suggestion1">
{this.state.suggestions.map((suggestion, index) => (
<div className={this.getSuggestionCss(index)} key={index} onClick={this.handleSuggestionClick} >
{suggestion
.toLowerCase()
.startsWith(this.state.suggestionTypedText) ? (
<div>
<b>{this.state.suggestionTypedText}</b>
{suggestion
.toLowerCase()
.replace(this.state.suggestionTypedText, "")}
</div>
) : (
<div dangerouslySetInnerHTML={this.getSuggestionHtml(suggestion)} />
)}
</div>
))}
</div>
<footer className="chat-footer" >
<div className="foot-footer">
Was I helpful ?
<span className="feedback" onClick={() => this.feedback("Yes")} >Yes</span><span>|</span><span className="feedback" onClick={() => this.feedback("No")}>No</span>
{
this.state.feedBack === "Yes" || this.state.feedBack === "No" ?
(
<div className="dialog" id="myform">
<div id="textfeedback">
<span id="closeFeedback" onClick={() => this.feedback("Close")}>X</span>
<p>{this.state.popupContent}</p>
<input type="text" id="feedbacktxtbox" required name="textfeedback" placeholder="Pleasure to hear from u!"
onChange={this.handleChange}
value={this.state.value} />
<button type="button" id="btnfeedback" onClick={() => this.feedback("Send")}>send</button>
</div>
</div>
) : null
}
</div>
</footer>
</div>
);
}
}
}
The chat input box is called the send box in Web Chat. Clearing the send box is just setting the send box with an empty string. This is done automatically when you click on the send button normally. You can see in the submit send box saga that submitting the send box means performing two actions: sending the message and setting the send box.
if (sendBoxValue) {
yield put(sendMessage(sendBoxValue.trim(), method, { channelData }));
yield put(setSendBox(''));
}
This means that if you use the SUBMIT_SEND_BOX action then the send box will be cleared automatically. Of course, if you want that to work with your autocomplete component then you'll need to set the send box with the autocompleted text before you submit it. Your other option is to just use the SET_SEND_BOX action with an empty string after you send the message.

Yields "TypeError: Cannot read property 'xxxx' of undefined" after running jest with Vue

I'm trying to make a test using jest with Vue.
the details below.
Problem:
Can't mount using shallowMount option.
Situation:
Run the test after mounting the component using shallowMount option that provides in Vue-test-utils.
Throw error "Cannot read property 'XXXX' of undefined
This is my test code.
import myComponent from '#/~';
import Vuex from 'vuex';
import Vuelidate from 'vuelidate';
import { shallowMount, createLocalVue } from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(Vuelidate);
describe('myComponent~', () => {
let store;
beforeEach(() => {
store = new Vuex.Store({
modules: {
user: {
namespaced: true,
getters: {
profile: () => {
const profile = { name: 'blahblah' };
return profile;
},
},
},
},
});
});
describe('profile.name is "blahblah"', () => {
it('return something~', () => {
const wrapper = shallowMount(myComponent, {
localVue,
store,
mocks: {
$api: {
options: {
testMethod() {
return new Promise((resolve, reject) => {
resolve('test');
});
},
},
},
$i18n: {
t() {
return {
EN: 'EN',
KO: 'KO',
JP: 'JA',
SC: 'zh-CN',
TC: 'tw-CN',
};
},
},
},
});
expect(wrapper.find('.profile').text()).toBe('blahblah');
});
I think the problem is that property isn't set as a specified value or an empty value like an array or object.
But I don't know how I set properly the properties in my logic.
For example,
when the error yields "Cannot read property 'images' of undefined",
I add to a wrapper in the relevant method like this.
exampleMethod() {
this.something = this.something.map(item => {
if (item.detailContent.images) { // <-- the added wrapper is here
~~~logic~~~~
}
})
}
But the undefined properties are so many, I also think this way is not proper.
How I do solve this problem?
added
These are details about the above example method:
exampleMethod() {
this.something = this.something.map(item => {
let passValidation = false;
let failValidation = false;
if (item.detailContent.images) {
if (this.detail.showLanguages.includes(item.code)) {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: true,
}
});
}
checkValidationPass = true;
} else {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: false,
}
});
}
checkValidationPass = false;
}
return {
...item,
required: passValidation,
warning: failValidation,
}
}
});
if (this.configId === 'OPTION2') {
this.checkOption2Validation();
} else if (this.configId === 'OPTION3') {
this.checkOption3Validation();
} else {
this.checkOption1Validation();
}
},
And this is 'this.something':
data() {
return {
something: []
}
}
The detailContent is set here.
setMethod() {
this.something = [
...this.otherthings,
];
this.something = this.something.map(item => {
let details1 = {};
if (this.configId === 'OPTION2') {
details1 = {
images: [
{ deviceType: 'PC', titleList: [null, null], imageType: 'IMAGE' },
{ deviceType: 'MOBILE', titleList: [null, null, null] }
]
};
} else if (this.configId === 'OPTION3') {
details1 = {
images: [
{ deviceType: 'PC' },
{ deviceType: 'MOBILE' }
],
links: { linkType: 'EMPTY' },
};
}
let details2 = {
mainTitle: {
content: null,
}
}
let checkValidation = false;
this.detail.detailLanguages.forEach(element => {
if (element.language === item.code) {
details1 = { ...element };
if (!!element.mainTitle) {
details2 = { ...element };
} else {
details2 = {
...details2,
...element
};
}
if (this.configId !== 'OPTION1') {
details1.images = details1.images.map(image => {
return {
...image,
required: true,
}
});
}
checkValidation = true;
}
});
return {
...item,
detailContent: this.configId !== 'OPTION1' ? details1 : details2,
required: false,
warning: false,
}
});
},

Trying to use this.props.dispatch and returns that is not a function

I am trying to delete an item from DB but when I access the function is saying _this.props.dispatch is not a function. (In '_this.props.dispatch((0, _actions.deleteJob)(_this.props.id))', '_this.props.dispatch' is undefined)
Here is my code where I am calling the function to delete my item. The function onDelete() I am calling it after user interaction.
class JobItem extends Component {
constructor(props) {
super(props)
this.state = {
activeRowKey: null,
deleting: false
};
}
onDelete = () => {
this.setState({deleting: true});
this.props.dispatch(deleteJob(this.props.id)); // HERE is the error
}
render() {
const swipeSettings = {
autoClose: true,
onClose: (secId, rowId, direction) => {
this.setState({ activeRowKey: null });
},
onOpen: (secId, rowId, direction) => {
this.setState({ activeRowKey: this.props.id });
},
right: [
{
onPress: () => {
const deletingRow = this.state.activeRowKey;
Alert.alert(
'Alert',
'Are you sure you want to delete?',
[
{text: 'No', onPress: () => console.log('Cancel Pressed'), style:'cancel'},
{text: 'Yes', onPress: () => {
this.onDelete();
// Refresh Job List
this.props.parentFlatList.refreshJobList(deletingRow);
}},
],
{ cancelable: true }
)
},
text: 'Delete', type: 'delete'
}
],
rowId: this.props._id,
sectionId: 1
}
And here is the deleteJob() function where it actually delete it from DB
export function deleteJob(job_id) {
return function (dispatch) {
return axios.delete(JOB_URL(user_id, job_id), {
headers: { authorization: token }
}).then((response) => {
dispatch(removeJob(job_id));
}).catch((err) => {
dispatch(addAlert("Couldn't delete job."));
});
};
}
JobItem
var renderJobs = () => {
return this.props.jobs.map((job) => {
return (
<JobItem
parentFlatList={this}
key={job._id}
title={job.title}
shortDescription={job.shortDescription}
logo={job.avatar}
company={job.company}
id={job._id}/>
)
})
}
var mapStateToProps = (state) => {
return {
jobs: state.jobs
}
}
module.exports = connect(mapStateToProps)(JobList);
Any idea how shall I solve this?
I think you forgot to pass dispatch to JobItem
<JobItem
parentFlatList={this}
key={job._id}
title={job.title}
shortDescription={job.shortDescription}
logo={job.avatar}
company={job.company}
id={job._id}
dispatch={this.props.dispatch} /* <-- this one here */
/>
One way to fix the problem is to put JobItem inside container.
Something like this
module.exports = connect(mapStateToProps,dispatch=>({dispatch}))(JobItem);

GraphQL Validation Error on mutation

I am trying to set up a mutation with modified code from the Relay Todo example.
When I try to compile I get the following error:
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Cannot query field "addCampaign" on type "Mutation".
Source:
>
> mutation AddCampaignMutation {addCampaign}
> ^^^
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Unknown type "AddCampaignPayload". Did you mean "AddTodoPayload" or "RenameTodoPayload"?
Source:
>
> fragment AddCampaignMutationRelayQL on AddCampaignPayload #relay(pattern: true) {
> ^^
I have duplicated the Todo code so I don't know why the Todo mutation is working correctly but my new Campaign test isn't.
This is my database.js file, I have removed the Todo related items to make the document easier to read:
export class Campaign {}
export class User {}
// Mock authenticated ID
const VIEWER_ID = 'me';
// Mock user data
const viewer = new User();
viewer.id = VIEWER_ID;
const usersById = {
[VIEWER_ID]: viewer,
};
// Mock campaign data
const campaignsById = {};
const campaignIdsByUser = {
[VIEWER_ID]: [],
};
let nextCampaignId = 0;
addCampaign('Campaign1');
addCampaign('Campaign2');
addCampaign('Campaign3');
addCampaign('Campaign4');
export function addCampaign(text) {
const campaign = new Campaign();
//campaign.complete = !!complete;
campaign.id = `${nextCampaignId++}`;
campaign.text = text;
campaignsById[campaign.id] = campaign;
campaignIdsByUser[VIEWER_ID].push(campaign.id);
return campaign.id;
}
export function getCampaign(id) {
return campaignsById[id];
}
export function getCampaigns(status = 'any') {
const campaigns = campaignIdsByUser[VIEWER_ID].map(id => campaignsById[id]);
if (status === 'any') {
return campaigns;
}
return campaigns.filter(campaign => campaign.complete === (status === 'completed'));
}
This is my schema.js file, again I have removed the Todo related items to make the document easier to read:
import {
GraphQLBoolean,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId,
} from 'graphql-relay';
import {
Campaign,
addCampaign,
getCampaign,
getCampaigns,
User,
getViewer,
} from './database';
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
} else if (type === 'Campaign') {
return getCampaign(id);
}
return null;
},
(obj) => {
if (obj instanceof User) {
return GraphQLUser;
} else if (obj instanceof Campaign) {
return GraphQLCampaign;
}
return null;
}
);
/**
* Define your own connection types here
*/
const GraphQLAddCampaignMutation = mutationWithClientMutationId({
name: 'AddCampaign',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
campaignEdge: {
type: GraphQLCampaignEdge,
resolve: ({localCampaignId}) => {
const campaign = getCampaign(localCampaignId);
return {
cursor: cursorForObjectInConnection(getCampaigns(), campaign),
node: campaign,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localCampaignId = addCampaign(text);
return {localCampaignId};
},
});
const GraphQLCampaign = new GraphQLObjectType({
name: 'Campaign',
description: 'Campaign integrated in our starter kit',
fields: () => ({
id: globalIdField('Campaign'),
text: {
type: GraphQLString,
description: 'Name of the campaign',
resolve: (obj) => obj.text,
}
}),
interfaces: [nodeInterface]
});
const {
connectionType: CampaignsConnection,
edgeType: GraphQLCampaignEdge,
} = connectionDefinitions({
name: 'Campaign',
nodeType: GraphQLCampaign,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
campaigns: {
type: CampaignsConnection,
args: {
...connectionArgs,
},
resolve: (obj, {...args}) =>
connectionFromArray(getCampaigns(), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
This is my AddCampaignMutation.js file:
import Relay from 'react-relay';
export default class AddCampaignMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
console.log('getMutation');
return Relay.QL`mutation{addCampaign}`;
}
getFatQuery() {
console.log('getFatQuery');
return Relay.QL`
fragment on AddCampaignPayload #relay(pattern: true) {
campaignEdge,
viewer {
campaigns,
},
}
`;
}
getConfigs() {
console.log('getConfigs');
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'campaigns',
edgeName: 'campaignEdge',
rangeBehaviors: ({orderby}) => {
if (orderby === 'newest') {
return 'prepend';
} else {
return 'append';
}
},
//rangeBehaviors: ({status}) => {
// if (status === 'completed') {
// return 'ignore';
// } else {
// return 'append';
// }
//},
}];
}
getVariables() {
console.log('getVariables');
return {
text: this.props.text,
};
}
getOptimisticResponse() {
console.log('getOptimisticResponse');
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
campaignEdge: {
node: {
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
And finally this is the app file that contains my text input and the call to AddCampaignMutation:
import AddTodoMutation from '../mutations/AddTodoMutation';
import AddCampaignMutation from '../mutations/AddCampaignMutation';
import TodoListFooter from './TodoListFooter';
import TodoTextInput from './TodoTextInput';
import React from 'react';
import Relay from 'react-relay';
class TodoApp extends React.Component {
_handleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddTodoMutation({text, viewer: this.props.viewer})
);
};
_campaignHandleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddCampaignMutation({text, viewer: this.props.viewer})
);
};
render() {
const hasTodos = this.props.viewer.totalCount > 0;
return (
<div>
<section className="todoapp">
<header className="header">
<TodoTextInput
autoFocus={true}
className="new-campaign"
onSave={this._campaignHandleTextInputSave}
placeholder="Campaign name"
/>
<h1>
todos
</h1>
<TodoTextInput
autoFocus={true}
className="new-todo"
onSave={this._handleTextInputSave}
placeholder="What needs to be done?"
/>
</header>
{this.props.children}
{hasTodos &&
<TodoListFooter
todos={this.props.viewer.todos}
viewer={this.props.viewer}
/>
}
</section>
</div>
);
}
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on User {
totalCount,
${AddTodoMutation.getFragment('viewer')},
${AddCampaignMutation.getFragment('viewer')},
${TodoListFooter.getFragment('viewer')},
}
`,
},
});
Well I feel a bit silly but I found out what the problem was. I didn't realise that my schema.json file was not updating!
If anyone has a similar problem make sure that the schema.json file is up to date by running the following command to rebuild it:
npm run-script update-schema

Categories

Resources