I am having a hard time figuring out how to do mutations on plain array via relay.
I am trying to add a new tag to a post.
It does not get updated on client-side after being successfully added on the server-side.
I have to manually reload to see the new tag.
I have tried both REQUIRED_CHILDREN and this.props.relay.forceFetch(), but to no avail.
Also, tried FIELDS_CHANGE for post.
GraphQL Schema:
Post {
id: ID!
text: String!
tags: [Tag!]!
}
Tag {
id: ID!
name: String!
}
AddTagToPostMutation:
static fragments = {
post: () => Relay.QL`
fragment on Post {
id
tags
}
`,
}
getMutation() {
return Relay.QL`mutation { addTagToPost }`;
}
getVariables() {
return {
name: this.props.tag.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`;
}
getConfigs() {
return [{
type: 'REQUIRED_CHILDREN',
children: [Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`],
}];
}
getOptimisticResponse() {
return {
tag: {
name: this.props.tag.name,
},
post: {
id: this.props.post.id,
},
};
}
As freiksenet already pointed out, FIELDS_CHANGE should be used in getConfigs() function. I took your schema, implemented the GraphQL types, server-side and client-side mutation to add tag to a post. The client-side gets updated successfully. I'm going to just elaborate the solution in my answer.
First, check your server-side mutation. My implementation uses graphql and graphql-relay libraries and looks like below. Notice that the output of the server-side mutation is a post to which a tag has been added. This post is the one whose ID was provided as input.
const AddTagToPostMutation = mutationWithClientMutationId({
name: 'AddTagToPost',
inputFields: {
postId: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
post: {
type: PostType,
resolve: ({id}) => getPost(id),
},
},
mutateAndGetPayload: ({postId, name}) => {
const id = fromGlobalId(postId).id;
addTagToPost(id, name);
return {id};
},
});
Using graphiql, you can test your mutation:
mutation {
addTagToPost(input:{
postId: "UG9zdDpwb3N0Mg=="
name:"a new tag name"
clientMutationId:"123244"}) {
post {
id
text
tags {
id
name
}
}
}
}
I added a field posts for all posts to the root query. Using graphiql, I first checked the post IDs and used one above.
Using react-relay, the client-side mutation code looks like below. It is passed a prop post whose ID is used as input variable in getVariables() function. In the getConfigs() function, we specify that post field has to be updated. The association between the payload field post and the passed prop post is established using FIELDS_CHANGE mutation type.
export default class AddTagToPostMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation{addTagToPost}`;
}
getVariables() {
return {
postId: this.props.post.id,
name: this.props.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostPayload {
post {
id,
tags {
id,
name,
}
}
}
`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
post: this.props.post.id,
},
}];
}
static fragments = {
post: () => Relay.QL`
fragment on Post {
id,
}
`,
};
}
The client-side mutation is invoked like this:
Relay.Store.commitUpdate(new AddTagToPostMutation({
post: postToModify,
name: tagName,
}));
I think you should just use FIELDS_CHANGE in such situations.
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {post: this.props.post.id},
}];
}
getOptimisticResponse() {
return {
post: {
id: this.props.post.id,
tags: [...this.props.post.tags, this.props.tag],
},
};
}
Related
I have an object in my schema which has an array like so:
type Foo #aws_cognito_user_pool {
id: ID!
name: String
comments: [Comment]
createdAt: AWSDateTime
}
the input and Comment Type look like so:
input FooInput #aws_cognito_user_pool {
name: String
comments: [CommentInput]
}
type Comment #aws_cognito_user_pool {
id: String
body: String
parent: String
createdAt: String
}
input CommentInput {
id: String
body: String
parent: String
createdAt: String
}
In my app I am using useReducer to manage state and apollo.
My Foo reducer looks like so
const INITIAL_FOO_STATE = () => {
name: "",
comments: [],
}
export const FooReducer = (state, action) => {
switch(action.field) {
case "name":
case "comments":
return {
...state, [action.field]: action.payload,
}
default:
return
}
}
in my component i have the add, update and delete functions like so:
const addComment = (body, parent) => {
CreateComment(
body,
parent,
)
.then((comment) => {
setComments([comment, ...comments]);
setActiveComment(null);
});
};
const updateComment = (body, commentId) => {
UpdateComment(body)
.then(() => {
const updatedComments = comments.map((item) => {
if (item.id === commentId) {
return {
...item,
body: body,
};
}
return item;
})
setComments(updatedComments);
setActiveComments(null);
})
};
const deleteComment = (commentId) => {
if (window.confirm("Are you sure you want to remove this comment?")) {
DeleteComment()
.then(() => {
const updatedComments = comments.filter((comment) => comment.id !== commentId);
setComments(updatedComments)
})
}
};
This works just fine on the frontend, but now want to dispatch these actions to my Foo.comments array on the backend and looking for some help as to the best way without the need of another table.
I was thinking of placing the dispatch's within their respective .then()
For anyone interested, the solution was simple.
I had to modify the comment type in schema to:
type Comment #aws_cognito_user_pool {
id: ID!
sourceId: ID!
body: String
parent: String
createdAt: String
}
input CommentInput {
body: String
parent: String
createdAt: String
}
then add the mutations and resolvers for create, update and delete of the Comment.
In the Foo comment field, a query resolver was needed.
This also meant a new table was needed for comments.
The table needed to have the sourceId as primary key and ID as sort key, with a global index setup on the sourceId
I swapped out the CRUD functions in my component with the appropriate Apollo mutations and done.
I want to create a slug for the url as soon as the user adds an event from the frontend. The slug is based on the name of the event. How to do that in V4 as the old method does not work now?
Slug creation link - old version
By following the article, it seems that you are trying to add lifecycle events to a model. You would need to make the following modifications to the article to make it work for v4.
After the creation of the article model via the admin dashboard, instead of adding the following file:
./api/article/models/Article.js
add:
./src/api/article/content-types/article/lifecycles.js
With the following:
const slugify = require('slugify');
module.exports = {
async beforeCreate(event) {
if (event.params.data.title) {
event.params.data.slug = slugify(event.params.data.title, {lower: true});
}
},
async beforeUpdate(event) {
if (event.params.data.title) {
event.params.data.slug = slugify(event.params.data.title, {lower: true});
}
},
};
Also the api endpoint changed in v4 so you would need to use:
GET /api/articles?filters[slug]=my-article-slug
This seem to work for me
Settings > Roles > Public > Slugify (checkbox findSlug)
config/plugins.js
module.exports = ({ env }) => ({
slugify: {
enabled: true,
config: {
contentTypes: {
page: {
field: "slug",
references: "name",
},
post: {
field: "slug",
references: "name",
},
category: {
field: "slug",
references: "name",
},
},
},
},
});
graphql
const POSTS = gql`
query GetPosts {
posts {
... on PostEntityResponseCollection {
data {
__typename
id
attributes {
__typename
name
slug
content
featuredImage {
data {
__typename
id
attributes {
url
alternativeText
caption
}
}
}
createdAt
}
}
}
}
}
`;
const POST = gql`
query GetPost($slug: String!) {
findSlug(modelName: "post", slug: $slug, publicationState: "live") {
... on PostEntityResponse {
data {
__typename
id
attributes {
createdAt
name
slug
content
seo {
__typename
id
title
description
blockSearchIndexing
}
categories {
__typename
data {
__typename
id
attributes {
__typename
name
slug
}
}
}
}
}
}
}
}
`;
I'm trying to add the BlockVariationPicker like in the WordPress Github example:
import { useSelect } from '#wordpress/data';
import {
__experimentalBlockVariationPicker as BlockVariationPicker,
store as blockEditorStore,
} from '#wordpress/block-editor';
const MyBlockVariationPicker = ( { blockName } ) => {
const variations = useSelect(
( select ) => {
const { getBlockVariations } = select( blocksStore );
return getBlockVariations( blockName, 'block' );
},
[ blockName ]
);
return <BlockVariationPicker variations={ variations } />;
};
In my edit function I'm adding:
{ MyBlockVariationPicker }
The block variation picker does not show.
I have already registered my bloc variations with scope block:
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
The block variations should show in { MyBlockVariationPicker } but the don't. Unfortunately there isn't much documentation about this. How can we render the variations of a block using the Block Variation Picker as shown in the Github example?
Both the Columns and Query block use __experimentalBlockVariationPicker and its a really nice component/UI and I agree, it there aren't many examples of how to use it, most likely as its still 'experimental' and still likely to change.
I found that both the Columns and Query blocks display the BlockVariationPicker by checking if the current block (by clientId) contains any InnerBlocks; if there are none, the BlockVariationPicker is shown. When using this component in your own block, you will need some attribute or property to check whether or not a variation has been selected.
I've put together a basic/working example using the structure of your my/testimonial block + variations and based on how the BlockVariationPicker is implemented in Columns block:
import { get } from 'lodash';
import { useSelect } from '#wordpress/data';
import { registerBlockType, registerBlockVariation, store as blocksStore } from '#wordpress/blocks';
import { useBlockProps, __experimentalBlockVariationPicker as BlockVariationPicker } from '#wordpress/block-editor';
// Create our own BlockVariationPicker
const MyBlockVariationPicker = ({ name, setAttributes }) => { // Note: We need "name" and "setAttributes" from edit() props
const { blockType, variations, defaultVariation } = useSelect(
(select) => {
const { getBlockVariations, getBlockType, getDefaultBlockVariation } = select(blocksStore);
return {
blockType: getBlockType(name),
defaultVariation: getDefaultBlockVariation(name, 'block'),
variations: getBlockVariations(name, 'block')
};
},
[name]
);
return <BlockVariationPicker
variations={variations}
icon={get(blockType, ['icon', 'src'])}
label={get(blockType, ['title'])}
onSelect={(nextVariation = defaultVariation) => {
if (nextVariation.attributes) {
setAttributes(nextVariation.attributes); // Use setAttributes to set the selected variation attributes
}
}}
/>;
};
// Register the Block Variations
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
icon: 'admin-comments', // Added icon so the variation is visibly different (optional)
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
isDefault: true
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
icon: 'admin-links',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
registerBlockType('my/testimonial', {
title: 'My Testimonial',
keywords: ['testimonial'],
icon: 'admin-post',
attributes: {
example: {
type: "string", // no default set, example is "undefined"
}
},
edit(props) {
const { attributes, setAttributes } = props;
// If example is undefined, show Variation Picker
if (attributes.example === undefined) {
return (
<MyBlockVariationPicker {...props} />
);
}
// Otherwise show the Editor
return (<div {...useBlockProps()}><h2>{attributes.example}</h2></div>);
},
save: ({ attributes }) => {
return <div {...useBlockProps.save()}><h2>{attributes.example}</h2></div>;
}
})
If you build the above javascript, the resulting block allows you to pick from the two variations on insertion:
I am using Apollo Server / Client and the cache does not seem to work on update mutations. (Create, Delete). The server gets updated but nothing happens on the front end. I have to reload the page to show the new item / show change of an item.
I followed the Apollo docs and modeled it after their sandbox implementation.
Let me know if you need more of my code, thank you.
Here is my code:
<form
onSubmit={(e) => {
e.preventDefault();
createUser(
{
variables: {
name: input.value,
email: input.value,
password: input.value
}
},
{
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
allUsers(existingUsers = []) {
const newUser = cache.writeFragment({
data: { createUser },
fragment: gql`
fragment NewUser on User {
name
email
}
`
});
return existingUsers.concat(newUser);
}
}
});
}
}
);
}}
>
You need to provide an id property in the writeFragment method. Here's the example on the docs:
client.writeFragment({
id: '5',
fragment: gql`
fragment MyTodo on Todo {
completed
}
`,
data: {
completed: true,
},
});
Also, writeFragment returns void, so you need to use readFragment to get the data you want, or just use the data available in the mutation's result
I have this simple query which works fine in my Graphql but I cannot pass data using relay to components and I don't know why :(
{
todolist { // todolist returns array of objects of todo
id
text
done
}
}
this is my code in an attempt to pass data in components using relay:
class TodoList extends React.Component {
render() {
return <ul>
{this.props.todos.todolist.map((todo) => {
<Todo todo={todo} />
})}
</ul>;
}
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on Query {
todolist {
id
text
done
}
}
`,
},
});
And lastly my schema
const Todo = new GraphQLObjectType({
name: 'Todo',
description: 'This contains list of todos which belong to its\' (Persons)users',
fields: () => {
return {
id: {
type: GraphQLInt,
resolve: (todo) => {
return todo.id;
}
},
text: {
type: GraphQLString,
resolve: (todo) => {
return todo.text;
}
},
done: {
type: GraphQLBoolean,
resolve: (todo) => {
return todo.done;
}
},
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
description: 'This is the root query',
fields: () => {
return {
todolist: {
type: new GraphQLList(Todo),
resolve: (root, args) => {
return Conn.models.todo.findAll({ where: args})
}
}
}
}
});
This code looks simple and I cannot see why this won't work and I have this error Uncaught TypeError: Cannot read property 'todolist' of undefined, but I configure todolist and I can query in my graphql, you can see the structure of the query is same, I don't know why this is not working?
todolist should be a connection type on Query. Also, your ids should be Relay global IDs. You will not have access to your objects' raw native id fields in Relay.
import {
connectionArgs,
connectionDefinitions,
globalIdField,
} from 'graphql-relay';
// I'm renaming Todo to TodoType
const TodoType = new GraphQLObjectType({
...,
fields: {
id: uidGlobalIdField('Todo'),
...
},
});
const {
connectionType: TodoConnection,
} = connectionDefinitions({ name: 'Todo', nodeType: TodoType });
// Also renaming Query to QueryType
const QueryType = new GraphQLObjectType({
...,
fields: {
id: globalIdField('Query', $queryId), // hard-code queryId if you only have one Query concept (Facebook thinks of this top level field as being a user, so the $queryId would be the user id in their world)
todos: { // Better than todoList; generally if it's plural in Relay it's assumed to be a connection or list
type: TodoConnection,
args: connectionArgs,
},
},
});
// Now, to be able to query off of QueryType
const viewerDefaultField = {
query: { // Normally this is called `viewer`, but `query` is ok (I think)
query: QueryType,
resolve: () => ({}),
description: 'The entry point into the graph',
}
};
export { viewerDefaultField };
The above is not fully complete (you'll likely also need to setup a node interface on one or more of your types, which will require node definitions), but it should answer your basic question and get you started.
It's a huge, huge pain to learn, but once you struggle through it it starts to make sense and you'll begin to love it over RESTful calls.