This commit is contained in:
David Price
2021-07-12 20:42:52 -07:00
parent c86e479ffe
commit 3dc6ccde15
27 changed files with 2883 additions and 1416 deletions

View File

@@ -3,6 +3,6 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@redwoodjs/api": "^0.34.0" "@redwoodjs/api": "^0.35.0"
} }
} }

View File

@@ -9,8 +9,10 @@ import services from 'src/services/**/*.{js,ts}'
import { getCurrentUser } from 'src/lib/auth' import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db' import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({ export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
getCurrentUser, getCurrentUser,
schema: makeMergedSchema({ schema: makeMergedSchema({
schemas, schemas,

View File

@@ -1,6 +1,11 @@
import { db } from 'src/lib/db' import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth' import { requireAuth } from 'src/lib/auth'
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
export const beforeResolver = (rules) => {
rules.add(requireAuth)
}
export const posts = () => { export const posts = () => {
return db.post.findMany() return db.post.findMany()
} }
@@ -12,14 +17,12 @@ export const post = ({ id }) => {
} }
export const createPost = ({ input }) => { export const createPost = ({ input }) => {
requireAuth()
return db.post.create({ return db.post.create({
data: input, data: input,
}) })
} }
export const updatePost = ({ id, input }) => { export const updatePost = ({ id, input }) => {
requireAuth()
return db.post.update({ return db.post.update({
data: input, data: input,
where: { id }, where: { id },
@@ -27,7 +30,6 @@ export const updatePost = ({ id, input }) => {
} }
export const deletePost = ({ id }) => { export const deletePost = ({ id }) => {
requireAuth()
return db.post.delete({ return db.post.delete({
where: { id }, where: { id },
}) })

View File

@@ -1,18 +1,6 @@
export const standard = defineScenario({ export const standard = defineScenario({
post: { post: {
first: { one: { title: 'String', body: 'String' },
title: 'First Post', two: { title: 'String', body: 'String' },
body: 'Lorem ipsum dolar sit amet',
},
},
})
export const withLongBody = defineScenario({
post: {
long: {
title: 'First Post',
body:
"I'm baby succulents single-origin coffee coloring book taxidermy sartorial. Irony woke tilde, gentrify you probably haven't heard of them tumblr typewriter enamel pin brunch viral live-edge. Pok pok plaid fingerstache, copper mug pitchfork pickled 3 wolf moon farm-to-table sustainable. Jean shorts freegan salvia tofu dreamcatcher vaporware asymmetrical +1. Pabst gentrify keytar ennui mixtape. Franzen tbh cred austin, health goth intelligentsia flannel pug you probably haven't heard of them 8-bit try-hard tumeric 3 wolf moon cray. Tumeric edison bulb distillery slow-carb subway tile authentic literally kinfolk venmo.\n Food truck pop-up ugh, tacos gluten-free lyft readymade four dollar toast brunch palo santo quinoa. Mixtape twee meh chicharrones lyft. Helvetica succulents brooklyn, farm-to-table +1 quinoa mlkshk vegan you probably haven't heard of them hella bespoke af. Tilde distillery taxidermy artisan, chillwave tofu direct trade raclette polaroid deep v sustainable. Chambray iPhone letterpress, helvetica XOXO wayfarers migas. Hoodie roof party tilde cornhole, heirloom normcore copper mug schlitz meditation YOLO lumbersexual franzen drinking vinegar gluten-free swag.",
},
}, },
}) })

View File

@@ -1,16 +1,41 @@
import { posts, post } from './posts' import { posts, post, createPost, updatePost, deletePost } from './posts'
describe('posts', () => { describe('posts', () => {
scenario('returns a list of posts', async (scenario) => { scenario('returns all posts', async (scenario) => {
const list = await posts() const result = await posts()
expect(list.length).toEqual(Object.keys(scenario.post).length) expect(result.length).toEqual(Object.keys(scenario.post).length)
expect(list[0].title).toEqual(scenario.post.first.title)
}) })
scenario('returns a single post by ID', async (scenario) => { scenario('returns a single post', async (scenario) => {
const record = await post({ id: scenario.post.first.id }) const result = await post({ id: scenario.post.one.id })
expect(record.title).toEqual(scenario.post.first.title) expect(result).toEqual(scenario.post.one)
})
scenario('creates a post', async () => {
const result = await createPost({
input: { title: 'String', body: 'String' },
})
expect(result.title).toEqual('String')
expect(result.body).toEqual('String')
})
scenario('updates a post', async (scenario) => {
const original = await post({ id: scenario.post.one.id })
const result = await updatePost({
id: original.id,
input: { title: 'String2' },
})
expect(result.title).toEqual('String2')
})
scenario('deletes a post', async (scenario) => {
const original = await deletePost({ id: scenario.post.one.id })
const result = await post({ id: original.id })
expect(result).toEqual(null)
}) })
}) })

View File

@@ -7,7 +7,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@redwoodjs/core": "^0.34.0" "@redwoodjs/core": "^0.35.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "@redwoodjs/eslint-config" "extends": "@redwoodjs/eslint-config"

View File

@@ -13,10 +13,10 @@
] ]
}, },
"dependencies": { "dependencies": {
"@redwoodjs/auth": "^0.34.0", "@redwoodjs/auth": "^0.35.0",
"@redwoodjs/forms": "^0.34.0", "@redwoodjs/forms": "^0.35.0",
"@redwoodjs/router": "^0.34.0", "@redwoodjs/router": "^0.35.0",
"@redwoodjs/web": "^0.34.0", "@redwoodjs/web": "^0.35.0",
"netlify-identity-widget": "^1.9.1", "netlify-identity-widget": "^1.9.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.2", "react": "^17.0.2",

View File

@@ -1,6 +1,6 @@
import { AuthProvider } from '@redwoodjs/auth' import { AuthProvider } from '@redwoodjs/auth'
import netlifyIdentity from 'netlify-identity-widget' import netlifyIdentity from 'netlify-identity-widget'
import { FatalErrorBoundary } from '@redwoodjs/web' import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage' import FatalErrorPage from 'src/pages/FatalErrorPage'
import { isBrowser } from '@redwoodjs/prerender/browserUtils' import { isBrowser } from '@redwoodjs/prerender/browserUtils'
@@ -14,11 +14,13 @@ isBrowser && netlifyIdentity.init()
const App = () => ( const App = () => (
<FatalErrorBoundary page={FatalErrorPage}> <FatalErrorBoundary page={FatalErrorPage}>
<AuthProvider client={netlifyIdentity} type="netlify"> <RedwoodProvider>
<RedwoodApolloProvider> <AuthProvider client={netlifyIdentity} type="netlify">
<Routes /> <RedwoodApolloProvider>
</RedwoodApolloProvider> <Routes />
</AuthProvider> </RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary> </FatalErrorBoundary>
) )

View File

@@ -8,6 +8,7 @@
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
import { Router, Route, Private, Set } from '@redwoodjs/router' import { Router, Route, Private, Set } from '@redwoodjs/router'
import PostsLayout from 'src/layouts/PostsLayout'
import BlogLayout from 'src/layouts/BlogLayout' import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => { const Routes = () => {
@@ -20,10 +21,12 @@ const Routes = () => {
<Route path="/" page={HomePage} name="home" /> <Route path="/" page={HomePage} name="home" />
</Set> </Set>
<Private unauthenticated="home"> <Private unauthenticated="home">
<Route path="/admin/posts/new" page={NewPostPage} name="newPost" /> <Set wrap={PostsLayout}>
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" /> <Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}" page={PostPage} name="post" /> <Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts" page={PostsPage} name="posts" /> <Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
</Private> </Private>
<Route notfound page={NotFoundPage} /> <Route notfound page={NotFoundPage} />
</Router> </Router>

View File

@@ -1,9 +1,10 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import PostForm from 'src/components/PostForm' import PostForm from 'src/components/Post/PostForm'
export const QUERY = gql` export const QUERY = gql`
query FIND_POST_BY_ID($id: Int!) { query FindPostById($id: Int!) {
post: post(id: $id) { post: post(id: $id) {
id id
title title
@@ -26,11 +27,10 @@ const UPDATE_POST_MUTATION = gql`
export const Loading = () => <div>Loading...</div> export const Loading = () => <div>Loading...</div>
export const Success = ({ post }) => { export const Success = ({ post }) => {
const { addMessage } = useFlash()
const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, { const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
onCompleted: () => { onCompleted: () => {
toast.success('Post updated')
navigate(routes.posts()) navigate(routes.posts())
addMessage('Post updated.', { classes: 'rw-flash-success' })
}, },
}) })

View File

@@ -1,6 +1,7 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import PostForm from 'src/components/PostForm' import PostForm from 'src/components/Post/PostForm'
const CREATE_POST_MUTATION = gql` const CREATE_POST_MUTATION = gql`
mutation CreatePostMutation($input: CreatePostInput!) { mutation CreatePostMutation($input: CreatePostInput!) {
@@ -11,11 +12,10 @@ const CREATE_POST_MUTATION = gql`
` `
const NewPost = () => { const NewPost = () => {
const { addMessage } = useFlash()
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, { const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
onCompleted: () => { onCompleted: () => {
toast.success('Post created')
navigate(routes.posts()) navigate(routes.posts())
addMessage('Post created.', { classes: 'rw-flash-success' })
}, },
}) })

View File

@@ -1,4 +1,5 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { Link, routes, navigate } from '@redwoodjs/router' import { Link, routes, navigate } from '@redwoodjs/router'
const DELETE_POST_MUTATION = gql` const DELETE_POST_MUTATION = gql`
@@ -30,11 +31,10 @@ const checkboxInputTag = (checked) => {
} }
const Post = ({ post }) => { const Post = ({ post }) => {
const { addMessage } = useFlash()
const [deletePost] = useMutation(DELETE_POST_MUTATION, { const [deletePost] = useMutation(DELETE_POST_MUTATION, {
onCompleted: () => { onCompleted: () => {
toast.success('Post deleted')
navigate(routes.posts()) navigate(routes.posts())
addMessage('Post deleted.', { classes: 'rw-flash-success' })
}, },
}) })

View File

@@ -1,7 +1,7 @@
import Post from 'src/components/Post' import Post from 'src/components/Post/Post'
export const QUERY = gql` export const QUERY = gql`
query FIND_POST_BY_ID($id: Int!) { query FindPostById($id: Int!) {
post: post(id: $id) { post: post(id: $id) {
id id
title title

View File

@@ -7,6 +7,12 @@ import {
Submit, Submit,
} from '@redwoodjs/forms' } from '@redwoodjs/forms'
const formatDatetime = (value) => {
if (value) {
return value.replace(/:\d{2}\.\d{3}\w/, '')
}
}
const PostForm = (props) => { const PostForm = (props) => {
const onSubmit = (data) => { const onSubmit = (data) => {
props.onSave(data, props?.post?.id) props.onSave(data, props?.post?.id)

View File

@@ -1,6 +1,9 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import { QUERY } from 'src/components/Post/PostsCell'
const DELETE_POST_MUTATION = gql` const DELETE_POST_MUTATION = gql`
mutation DeletePostMutation($id: Int!) { mutation DeletePostMutation($id: Int!) {
deletePost(id: $id) { deletePost(id: $id) {
@@ -36,16 +39,20 @@ const checkboxInputTag = (checked) => {
} }
const PostsList = ({ posts }) => { const PostsList = ({ posts }) => {
const { addMessage } = useFlash()
const [deletePost] = useMutation(DELETE_POST_MUTATION, { const [deletePost] = useMutation(DELETE_POST_MUTATION, {
onCompleted: () => { onCompleted: () => {
addMessage('Post deleted.', { classes: 'rw-flash-success' }) toast.success('Post deleted')
}, },
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}) })
const onDeleteClick = (id) => { const onDeleteClick = (id) => {
if (confirm('Are you sure you want to delete post ' + id + '?')) { if (confirm('Are you sure you want to delete post ' + id + '?')) {
deletePost({ variables: { id }, refetchQueries: ['POSTS'] }) deletePost({ variables: { id } })
} }
} }

View File

@@ -1,6 +1,6 @@
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import Posts from 'src/components/Posts' import Posts from 'src/components/Post/Posts'
export const QUERY = gql` export const QUERY = gql`
query POSTS { query POSTS {

View File

@@ -1,10 +1,10 @@
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import { Flash } from '@redwoodjs/web' import { Toaster } from '@redwoodjs/web/toast'
const PostsLayout = (props) => { const PostsLayout = (props) => {
return ( return (
<div className="rw-scaffold"> <div className="rw-scaffold">
<Flash timeout={1000} /> <Toaster />
<header className="rw-header"> <header className="rw-header">
<h1 className="rw-heading rw-heading-primary"> <h1 className="rw-heading rw-heading-primary">
<Link to={routes.posts()} className="rw-link"> <Link to={routes.posts()} className="rw-link">

View File

@@ -1,12 +0,0 @@
import PostsLayout from 'src/layouts/PostsLayout'
import EditPostCell from 'src/components/EditPostCell'
const EditPostPage = ({ id }) => {
return (
<PostsLayout>
<EditPostCell id={id} />
</PostsLayout>
)
}
export default EditPostPage

View File

@@ -1,12 +0,0 @@
import PostsLayout from 'src/layouts/PostsLayout'
import NewPost from 'src/components/NewPost'
const NewPostPage = () => {
return (
<PostsLayout>
<NewPost />
</PostsLayout>
)
}
export default NewPostPage

View File

@@ -0,0 +1,7 @@
import EditPostCell from 'src/components/Post/EditPostCell'
const EditPostPage = ({ id }) => {
return <EditPostCell id={id} />
}
export default EditPostPage

View File

@@ -0,0 +1,7 @@
import NewPost from 'src/components/Post/NewPost'
const NewPostPage = () => {
return <NewPost />
}
export default NewPostPage

View File

@@ -0,0 +1,7 @@
import PostCell from 'src/components/Post/PostCell'
const PostPage = ({ id }) => {
return <PostCell id={id} />
}
export default PostPage

View File

@@ -0,0 +1,7 @@
import PostsCell from 'src/components/Post/PostsCell'
const PostsPage = () => {
return <PostsCell />
}
export default PostsPage

View File

@@ -1,12 +0,0 @@
import PostsLayout from 'src/layouts/PostsLayout'
import PostCell from 'src/components/PostCell'
const PostPage = ({ id }) => {
return (
<PostsLayout>
<PostCell id={id} />
</PostsLayout>
)
}
export default PostPage

View File

@@ -1,12 +0,0 @@
import PostsLayout from 'src/layouts/PostsLayout'
import PostsCell from 'src/components/PostsCell'
const PostsPage = () => {
return (
<PostsLayout>
<PostsCell />
</PostsLayout>
)
}
export default PostsPage

View File

@@ -6,9 +6,6 @@
.rw-scaffold ::after, .rw-scaffold ::after,
.rw-scaffold ::before { .rw-scaffold ::before {
box-sizing: inherit; box-sizing: inherit;
border-width: 0;
border-style: solid;
border-color: #e2e8f0;
} }
.rw-scaffold main { .rw-scaffold main {
color: #4a5568; color: #4a5568;
@@ -102,35 +99,6 @@
color: #1a202c; color: #1a202c;
text-decoration: underline; text-decoration: underline;
} }
.rw-flash-message {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin: 0 1rem;
padding: 1rem;
background: #e2e8f0;
border-radius: 0.25rem;
}
.rw-flash-message .rw-flash-message-dismiss {
font-size: 1.25rem;
font-weight: 600;
line-height: 1;
margin-right: 0.25rem;
transform-origin: center;
transform: rotate(45deg);
cursor: pointer;
}
.rw-flash-message .rw-flash-message-dismiss:hover {
opacity: 0.7;
}
.rw-flash-message.rw-flash-success {
background: #48bb78;
color: #fff;
}
.rw-flash-message.rw-flash-error {
background: #e53e3e;
color: #fff;
}
.rw-form-wrapper { .rw-form-wrapper {
box-sizing: border-box; box-sizing: border-box;
font-size: 0.875rem; font-size: 0.875rem;
@@ -238,8 +206,9 @@
border-radius: 0.25rem; border-radius: 0.25rem;
outline: none; outline: none;
} }
.rw-input[type='checkbox'] { .rw-input[type='checkbox'],
width: initial; .rw-input[type='radio'] {
width: 1rem;
margin-left: 0; margin-left: 0;
} }
.rw-input:focus { .rw-input:focus {

4027
yarn.lock

File diff suppressed because it is too large Load Diff