bunch of stuff
This commit is contained in:
28
web/src/components/BlogPostsCell/BlogPostsCell.js
Normal file
28
web/src/components/BlogPostsCell/BlogPostsCell.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export const QUERY = gql`
|
||||
query BlogPostsQuery {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ posts }) => {
|
||||
return posts.map((post) => (
|
||||
<article key={post.id}>
|
||||
<header>
|
||||
<h2>{post.title}</h2>
|
||||
</header>
|
||||
<p>{post.body}</p>
|
||||
<div>Posted on: {post.createdAt.split('T')[0]}</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
6
web/src/components/BlogPostsCell/BlogPostsCell.mock.js
Normal file
6
web/src/components/BlogPostsCell/BlogPostsCell.mock.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
blogPosts: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
20
web/src/components/BlogPostsCell/BlogPostsCell.stories.js
Normal file
20
web/src/components/BlogPostsCell/BlogPostsCell.stories.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostsCell'
|
||||
import { standard } from './BlogPostsCell.mock'
|
||||
|
||||
export const loading = () => {
|
||||
return Loading ? <Loading /> : null
|
||||
}
|
||||
|
||||
export const empty = () => {
|
||||
return Empty ? <Empty /> : null
|
||||
}
|
||||
|
||||
export const failure = () => {
|
||||
return Failure ? <Failure error={new Error('Oh no')} /> : null
|
||||
}
|
||||
|
||||
export const success = () => {
|
||||
return Success ? <Success {...standard()} /> : null
|
||||
}
|
||||
|
||||
export default { title: 'Cells/BlogPostsCell' }
|
||||
26
web/src/components/BlogPostsCell/BlogPostsCell.test.js
Normal file
26
web/src/components/BlogPostsCell/BlogPostsCell.test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@redwoodjs/testing'
|
||||
import { Loading, Empty, Failure, Success } from './BlogPostsCell'
|
||||
import { standard } from './BlogPostsCell.mock'
|
||||
|
||||
describe('BlogPostsCell', () => {
|
||||
test('Loading renders successfully', () => {
|
||||
render(<Loading />)
|
||||
// Use screen.debug() to see output
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Empty renders successfully', async () => {
|
||||
render(<Empty />)
|
||||
expect(screen.getByText('Empty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Failure renders successfully', async () => {
|
||||
render(<Failure error={new Error('Oh no')} />)
|
||||
expect(screen.getByText(/Oh no/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Success renders successfully', async () => {
|
||||
render(<Success blogPosts={standard().blogPosts} />)
|
||||
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
48
web/src/components/EditPostCell/EditPostCell.js
Normal file
48
web/src/components/EditPostCell/EditPostCell.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PostForm from 'src/components/PostForm'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_POST_BY_ID($id: Int!) {
|
||||
post: post(id: $id) {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const UPDATE_POST_MUTATION = gql`
|
||||
mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
|
||||
updatePost(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Success = ({ post }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post updated.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (input, id) => {
|
||||
updatePost({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">Edit Post {post.id}</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PostForm post={post} onSave={onSave} error={error} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
web/src/components/NewPost/NewPost.js
Normal file
38
web/src/components/NewPost/NewPost.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import PostForm from 'src/components/PostForm'
|
||||
|
||||
const CREATE_POST_MUTATION = gql`
|
||||
mutation CreatePostMutation($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NewPost = () => {
|
||||
const { addMessage } = useFlash()
|
||||
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post created.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onSave = (input) => {
|
||||
createPost({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New Post</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<PostForm onSave={onSave} loading={loading} error={error} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPost
|
||||
95
web/src/components/Post/Post.js
Normal file
95
web/src/components/Post/Post.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_POST_MUTATION = gql`
|
||||
mutation DeletePostMutation($id: Int!) {
|
||||
deletePost(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const jsonDisplay = (obj) => {
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(obj, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
const timeTag = (datetime) => {
|
||||
return (
|
||||
<time dateTime={datetime} title={datetime}>
|
||||
{new Date(datetime).toUTCString()}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const Post = ({ post }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
navigate(routes.posts())
|
||||
addMessage('Post deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete post ' + id + '?')) {
|
||||
deletePost({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Post {post.id} Detail
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{post.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{post.title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Body</th>
|
||||
<td>{post.body}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<td>{timeTag(post.createdAt)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
<Link
|
||||
to={routes.editPost({ id: post.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(post.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Post
|
||||
20
web/src/components/PostCell/PostCell.js
Normal file
20
web/src/components/PostCell/PostCell.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Post from 'src/components/Post'
|
||||
|
||||
export const QUERY = gql`
|
||||
query FIND_POST_BY_ID($id: Int!) {
|
||||
post: post(id: $id) {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Post not found</div>
|
||||
|
||||
export const Success = ({ post }) => {
|
||||
return <Post post={post} />
|
||||
}
|
||||
67
web/src/components/PostForm/PostForm.js
Normal file
67
web/src/components/PostForm/PostForm.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Form,
|
||||
FormError,
|
||||
FieldError,
|
||||
Label,
|
||||
TextField,
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
|
||||
const PostForm = (props) => {
|
||||
const onSubmit = (data) => {
|
||||
props.onSave(data, props?.post?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} error={props.error}>
|
||||
<FormError
|
||||
error={props.error}
|
||||
wrapperClassName="rw-form-error-wrapper"
|
||||
titleClassName="rw-form-error-title"
|
||||
listClassName="rw-form-error-list"
|
||||
/>
|
||||
|
||||
<Label
|
||||
name="title"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Title
|
||||
</Label>
|
||||
<TextField
|
||||
name="title"
|
||||
defaultValue={props.post?.title}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="title" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="body"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Body
|
||||
</Label>
|
||||
<TextField
|
||||
name="body"
|
||||
defaultValue={props.post?.body}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
<FieldError name="body" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||
Save
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostForm
|
||||
105
web/src/components/Posts/Posts.js
Normal file
105
web/src/components/Posts/Posts.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
const DELETE_POST_MUTATION = gql`
|
||||
mutation DeletePostMutation($id: Int!) {
|
||||
deletePost(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MAX_STRING_LENGTH = 150
|
||||
|
||||
const truncate = (text) => {
|
||||
let output = text
|
||||
if (text && text.length > MAX_STRING_LENGTH) {
|
||||
output = output.substring(0, MAX_STRING_LENGTH) + '...'
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const jsonTruncate = (obj) => {
|
||||
return truncate(JSON.stringify(obj, null, 2))
|
||||
}
|
||||
|
||||
const timeTag = (datetime) => {
|
||||
return (
|
||||
<time dateTime={datetime} title={datetime}>
|
||||
{new Date(datetime).toUTCString()}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxInputTag = (checked) => {
|
||||
return <input type="checkbox" checked={checked} disabled />
|
||||
}
|
||||
|
||||
const PostsList = ({ posts }) => {
|
||||
const { addMessage } = useFlash()
|
||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
||||
onCompleted: () => {
|
||||
addMessage('Post deleted.', { classes: 'rw-flash-success' })
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id) => {
|
||||
if (confirm('Are you sure you want to delete post ' + id + '?')) {
|
||||
deletePost({ variables: { id }, refetchQueries: ['POSTS'] })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment rw-table-wrapper-responsive">
|
||||
<table className="rw-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Title</th>
|
||||
<th>Body</th>
|
||||
<th>Created at</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>{truncate(post.id)}</td>
|
||||
<td>{truncate(post.title)}</td>
|
||||
<td>{truncate(post.body)}</td>
|
||||
<td>{timeTag(post.createdAt)}</td>
|
||||
<td>
|
||||
<nav className="rw-table-actions">
|
||||
<Link
|
||||
to={routes.post({ id: post.id })}
|
||||
title={'Show post ' + post.id + ' detail'}
|
||||
className="rw-button rw-button-small"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editPost({ id: post.id })}
|
||||
title={'Edit post ' + post.id}
|
||||
className="rw-button rw-button-small rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<a
|
||||
href="#"
|
||||
title={'Delete post ' + post.id}
|
||||
className="rw-button rw-button-small rw-button-red"
|
||||
onClick={() => onDeleteClick(post.id)}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</nav>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostsList
|
||||
31
web/src/components/PostsCell/PostsCell.js
Normal file
31
web/src/components/PostsCell/PostsCell.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import Posts from 'src/components/Posts'
|
||||
|
||||
export const QUERY = gql`
|
||||
query POSTS {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<div className="rw-text-center">
|
||||
{'No posts yet. '}
|
||||
<Link to={routes.newPost()} className="rw-link">
|
||||
{'Create one?'}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Success = ({ posts }) => {
|
||||
return <Posts posts={posts} />
|
||||
}
|
||||
Reference in New Issue
Block a user