Skip to content

Commit f217418

Browse files
committed
Split post body into parts to test JSON API included objects editing
1 parent 951914c commit f217418

File tree

18 files changed

+186
-39
lines changed

18 files changed

+186
-39
lines changed

app/models/post.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
class Post < ApplicationRecord
22
belongs_to :category
33
has_many :comments, dependent: :destroy
4+
has_many :parts, class_name: PostPart, dependent: :destroy
45

56
validates :title, presence: true, uniqueness: true
6-
validates :body, presence: true
7+
validates :parts, presence: true
78

89
scope :title_contains, -> (value) { where('title ILIKE ?', "%#{value.join}%") }
910
end

app/models/post_part.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class PostPart < ApplicationRecord
2+
belongs_to :post
3+
4+
validates :body, presence: true
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class PostPartResource < JSONAPI::Resource
2+
attributes :body
3+
4+
has_one :post
5+
end

app/resources/post_resource.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ class PostResource < JSONAPI::Resource
44
attributes :title, :body, :created_at
55

66
has_many :comments
7+
has_many :parts
78
has_one :category
89

910
paginator :paged
1011

1112
filters :category
1213
custom_filter :title_contains
1314
end
15+

client/src/api/__snapshots__/normalize.spec.js.snap

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,37 @@ Object {
3434
"type": "categories",
3535
},
3636
},
37+
"parts": Object {
38+
"data": Array [
39+
Object {
40+
"id": "1",
41+
"type": "postParts",
42+
},
43+
Object {
44+
"id": "2",
45+
"type": "postParts",
46+
},
47+
],
48+
},
3749
},
3850
"type": "posts",
3951
},
52+
"included": Array [
53+
Object {
54+
"attributes": Object {
55+
"body": "Part 1",
56+
},
57+
"id": "1",
58+
"type": "postParts",
59+
},
60+
Object {
61+
"attributes": Object {
62+
"body": "Part 2",
63+
},
64+
"id": "2",
65+
"type": "postParts",
66+
},
67+
],
4068
}
4169
`;
4270

@@ -48,6 +76,16 @@ Object {
4876
"name": undefined,
4977
},
5078
"id": "1",
79+
"parts": Array [
80+
Object {
81+
"body": "Part 1",
82+
"id": "1",
83+
},
84+
Object {
85+
"body": "Part 2",
86+
"id": "2",
87+
},
88+
],
5189
"title": "Title 1",
5290
}
5391
`;

client/src/api/client.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
castArray,
55
get,
66
groupBy,
7-
values,
87
keys,
9-
zipObject,
108
pick,
9+
set,
10+
values,
11+
zipObject,
1112
} from 'lodash';
1213

1314
import { denormalize, normalize } from './normalize';
@@ -75,15 +76,16 @@ const normalizeResponse = (response) => {
7576
const normalizeErrors = (response) => {
7677
throw get(response, 'response.data.errors')
7778
.reduce((errors, error) => {
78-
const attribute = /attributes\/(.*)$/.exec(get(error, 'source.pointer'))[1];
79-
errors[attribute] = error.title;
79+
const attribute = /\/data\/[a-z]*\/(.*)$/.exec(get(error, 'source.pointer'))[1];
80+
set(errors, attribute.split('/'), error.title);
8081
return errors;
8182
}, {});
8283
};
8384

8485
export default (requestType, payload, meta) => {
8586
const {
8687
url = `${meta.key}`,
88+
include,
8789
} = meta;
8890

8991
const params = payload;
@@ -104,13 +106,13 @@ export default (requestType, payload, meta) => {
104106
}).then(normalizeResponse).then(res => ({ ...res, params }));
105107
case CREATE:
106108
return client({
107-
url: withParams(url),
109+
url: withParams(url, { include }),
108110
method: 'POST',
109111
data: denormalize(meta.key, payload),
110112
}).then(normalizeResponse).catch(normalizeErrors);
111113
case UPDATE: {
112114
return client({
113-
url: withParams(`${url}/${payload.id}`),
115+
url: withParams(`${url}/${payload.id}`, { include }),
114116
method: 'PUT',
115117
data: denormalize(meta.key, payload),
116118
}).then(normalizeResponse).catch(normalizeErrors);

client/src/api/normalize.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,28 @@ const serializers = {
1717
posts: {
1818
serializer: new Serializer('posts', {
1919
keyForAttribute: 'camelCase',
20+
typeForAttribute: (attribute) => {
21+
switch(attribute) {
22+
case 'parts': return 'postParts';
23+
default: attribute;
24+
}
25+
},
2026
attributes: [
2127
'title',
2228
'body',
2329
'category',
30+
'parts',
2431
],
2532
category: {
2633
ref: 'id',
2734
included: false,
2835
attributes: ['name'],
2936
},
37+
parts: {
38+
ref: 'id',
39+
included: true,
40+
attributes: ['body'],
41+
},
3042
}),
3143
deserializer: new Deserializer({
3244
keyForAttribute: 'camelCase',
@@ -36,6 +48,24 @@ const serializers = {
3648
name: relationship.name,
3749
}),
3850
},
51+
parts: {
52+
valueForRelationship: relationship => ({
53+
id: relationship.id,
54+
body: relationship.body,
55+
}),
56+
},
57+
}),
58+
},
59+
60+
postParts: {
61+
serializer: new Serializer('postParts', {
62+
keyForAttribute: 'camelCase',
63+
attributes: [
64+
'body',
65+
],
66+
}),
67+
deserializer: new Deserializer({
68+
keyForAttribute: 'camelCase',
3969
}),
4070
},
4171

client/src/api/normalize.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,15 @@ describe('normalize', () => {
2525
id: 11,
2626
name: 'Category 11',
2727
},
28+
parts: [
29+
{
30+
body: 'Part 1',
31+
id: 1,
32+
},
33+
{
34+
body: 'Part 2',
35+
id: 2,
36+
},
37+
],
2838
});
2939
});

client/src/components/Posts/PostEdit.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ export class PostEdit extends Component {
1818
fetchCategories();
1919

2020
if (params.id) {
21-
fetchResource({ id: params.id, include: 'category' });
21+
fetchResource({ id: params.id, include: 'category,parts' });
2222
}
2323
}
2424

2525
render() {
2626
const { isNew, resource, onSubmit, categories } = this.props;
27+
const onSubmitWithInclude = (values) => onSubmit(values, { include: 'parts' });
2728

2829
return (
2930
<div>
3031
<EditHeader {...this.props}>{ isNew ? 'New Post' : resource.title }</EditHeader>
31-
<PostForm initialValues={resource} categories={categories} onSubmit={onSubmit} />
32+
<PostForm initialValues={resource} categories={categories} onSubmit={onSubmitWithInclude} />
3233
</div>
3334
);
3435
}
@@ -44,5 +45,5 @@ export const mapDispatchToProps = dispatch => ({
4445
});
4546

4647
export default connect(mapStateToProps, mapDispatchToProps)(
47-
withResource('posts')(PostEdit),
48+
withResource('posts', { include: 'parts' })(PostEdit),
4849
);

client/src/components/Posts/PostForm.js

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
import React, { Component, PropTypes } from 'react';
22
import { isEmpty } from 'lodash';
3-
import { Field, reduxForm } from 'redux-form';
4-
import { Button, Form } from 'reactstrap';
3+
import { Field, FieldArray, reduxForm } from 'redux-form';
4+
import { Button, Form, FormFeedback, Col, Row } from 'reactstrap';
55

66
import { InputField, TextArea, SelectField, required } from '../../forms';
77

8-
class PostForm extends Component {
8+
export const PostFormParts = ({ fields, meta: { error } }) => (
9+
<div className="my-3">
10+
{fields.map((part, index) =>
11+
<Row key={index}>
12+
<Col xs="10">
13+
<Field
14+
name={`${part}.body`}
15+
component={TextArea}
16+
rows={5}
17+
/>
18+
</Col>
19+
<Col xs="2">
20+
<Button color="danger" onClick={() => fields.remove(index)}>X</Button>
21+
</Col>
22+
</Row>
23+
)}
24+
{error && <FormFeedback>{error}</FormFeedback>}
25+
<Button onClick={() => fields.push()}>Add Part</Button>
26+
</div>
27+
);
28+
29+
export class PostForm extends Component {
930
render() {
1031
const { categories, handleSubmit, pristine, reset, submitting } = this.props;
1132

@@ -18,7 +39,7 @@ class PostForm extends Component {
1839
})));
1940

2041
return (
21-
<Form onSubmit={handleSubmit}>
42+
<Form onSubmit={handleSubmit} className="py-3">
2243
<Field
2344
name="title"
2445
label="Title"
@@ -30,13 +51,11 @@ class PostForm extends Component {
3051
component={SelectField}
3152
options={categoriesOptions}
3253
/>
33-
<Field
34-
name="body"
35-
label="Body"
36-
component={TextArea}
37-
rows="10"
54+
<FieldArray
55+
name="parts"
56+
component={PostFormParts}
3857
/>
39-
<Button disabled={pristine || submitting} color="primary">Submit</Button>
58+
<Button disabled={pristine || submitting} color="primary" className="mr-2">Submit</Button>
4059
<Button disabled={pristine || submitting} onClick={reset}>Undo Changes</Button>
4160
</Form>
4261
);
@@ -47,7 +66,7 @@ const validate = (values) => {
4766
const errors = required(values,
4867
'title',
4968
'category.id',
50-
'body',
69+
'parts',
5170
);
5271
return errors;
5372
};

0 commit comments

Comments
 (0)