Compare commits
No commits in common. "5251a637de11f5961a09be02bb2948382aa75d69" and "437e49b842f99bd73dc1590de6193cac09297bf6" have entirely different histories.
5251a637de
...
437e49b842
@ -1,8 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Post" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"title" TEXT NOT NULL,
|
|
||||||
"body" TEXT NOT NULL,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL
|
|
||||||
);
|
|
@ -52,11 +52,3 @@ model Identity {
|
|||||||
@@unique([provider, uid])
|
@@unique([provider, uid])
|
||||||
@@index(userId)
|
@@index(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Post {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
title String
|
|
||||||
body String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
@ -6,20 +6,6 @@ import type { DbAuthHandlerOptions, UserType } from '@redwoodjs/auth-dbauth-api'
|
|||||||
import { cookieName } from 'src/lib/auth'
|
import { cookieName } from 'src/lib/auth'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
export const cookie = {
|
|
||||||
attributes: {
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: '/',
|
|
||||||
SameSite: 'Strict',
|
|
||||||
Secure: process.env.NODE_ENV !== 'development',
|
|
||||||
|
|
||||||
// If you need to allow other domains (besides the api side) access to
|
|
||||||
// the dbAuth session cookie:
|
|
||||||
// Domain: 'example.com',
|
|
||||||
},
|
|
||||||
name: cookieName,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handler = async (
|
export const handler = async (
|
||||||
event: APIGatewayProxyEvent,
|
event: APIGatewayProxyEvent,
|
||||||
context: Context
|
context: Context
|
||||||
@ -197,7 +183,19 @@ export const handler = async (
|
|||||||
|
|
||||||
// Specifies attributes on the cookie that dbAuth sets in order to remember
|
// Specifies attributes on the cookie that dbAuth sets in order to remember
|
||||||
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
||||||
cookie,
|
cookie: {
|
||||||
|
attributes: {
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: '/',
|
||||||
|
SameSite: 'Strict',
|
||||||
|
Secure: process.env.NODE_ENV !== 'development',
|
||||||
|
|
||||||
|
// If you need to allow other domains (besides the api side) access to
|
||||||
|
// the dbAuth session cookie:
|
||||||
|
// Domain: 'example.com',
|
||||||
|
},
|
||||||
|
name: cookieName,
|
||||||
|
},
|
||||||
|
|
||||||
forgotPassword: forgotPasswordOptions,
|
forgotPassword: forgotPasswordOptions,
|
||||||
login: loginOptions,
|
login: loginOptions,
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
export const schema = gql`
|
|
||||||
type Post {
|
|
||||||
id: Int!
|
|
||||||
title: String!
|
|
||||||
body: String!
|
|
||||||
createdAt: DateTime!
|
|
||||||
updatedAt: DateTime!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
posts: [Post!]! @requireAuth
|
|
||||||
post(id: Int!): Post @requireAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
input CreatePostInput {
|
|
||||||
title: String!
|
|
||||||
body: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input UpdatePostInput {
|
|
||||||
title: String
|
|
||||||
body: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
createPost(input: CreatePostInput!): Post! @requireAuth
|
|
||||||
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
|
|
||||||
deletePost(id: Int!): Post! @requireAuth
|
|
||||||
}
|
|
||||||
`
|
|
@ -1,23 +0,0 @@
|
|||||||
import type { Prisma, Post } from '@prisma/client'
|
|
||||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
|
||||||
|
|
||||||
export const standard = defineScenario<Prisma.PostCreateArgs>({
|
|
||||||
post: {
|
|
||||||
one: {
|
|
||||||
data: {
|
|
||||||
title: 'String',
|
|
||||||
body: 'String',
|
|
||||||
updatedAt: '2024-10-04T07:38:59.006Z',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
two: {
|
|
||||||
data: {
|
|
||||||
title: 'String',
|
|
||||||
body: 'String',
|
|
||||||
updatedAt: '2024-10-04T07:38:59.006Z',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type StandardScenario = ScenarioData<Post, 'post'>
|
|
@ -1,55 +0,0 @@
|
|||||||
import type { Post } from '@prisma/client'
|
|
||||||
|
|
||||||
import { posts, post, createPost, updatePost, deletePost } from './posts'
|
|
||||||
import type { StandardScenario } from './posts.scenarios'
|
|
||||||
|
|
||||||
// Generated boilerplate tests do not account for all circumstances
|
|
||||||
// and can fail without adjustments, e.g. Float.
|
|
||||||
// Please refer to the RedwoodJS Testing Docs:
|
|
||||||
// https://redwoodjs.com/docs/testing#testing-services
|
|
||||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
|
||||||
|
|
||||||
describe('posts', () => {
|
|
||||||
scenario('returns all posts', async (scenario: StandardScenario) => {
|
|
||||||
const result = await posts()
|
|
||||||
|
|
||||||
expect(result.length).toEqual(Object.keys(scenario.post).length)
|
|
||||||
})
|
|
||||||
|
|
||||||
scenario('returns a single post', async (scenario: StandardScenario) => {
|
|
||||||
const result = await post({ id: scenario.post.one.id })
|
|
||||||
|
|
||||||
expect(result).toEqual(scenario.post.one)
|
|
||||||
})
|
|
||||||
|
|
||||||
scenario('creates a post', async () => {
|
|
||||||
const result = await createPost({
|
|
||||||
input: {
|
|
||||||
title: 'String',
|
|
||||||
body: 'String',
|
|
||||||
updatedAt: '2024-10-04T07:38:58.985Z',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.title).toEqual('String')
|
|
||||||
expect(result.body).toEqual('String')
|
|
||||||
expect(result.updatedAt).toEqual(new Date('2024-10-04T07:38:58.985Z'))
|
|
||||||
})
|
|
||||||
|
|
||||||
scenario('updates a post', async (scenario: StandardScenario) => {
|
|
||||||
const original = (await post({ id: scenario.post.one.id })) as Post
|
|
||||||
const result = await updatePost({
|
|
||||||
id: original.id,
|
|
||||||
input: { title: 'String2' },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.title).toEqual('String2')
|
|
||||||
})
|
|
||||||
|
|
||||||
scenario('deletes a post', async (scenario: StandardScenario) => {
|
|
||||||
const original = (await deletePost({ id: scenario.post.one.id })) as Post
|
|
||||||
const result = await post({ id: original.id })
|
|
||||||
|
|
||||||
expect(result).toEqual(null)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,32 +0,0 @@
|
|||||||
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
|
|
||||||
|
|
||||||
import { db } from 'src/lib/db'
|
|
||||||
|
|
||||||
export const posts: QueryResolvers['posts'] = () => {
|
|
||||||
return db.post.findMany()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const post: QueryResolvers['post'] = ({ id }) => {
|
|
||||||
return db.post.findUnique({
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createPost: MutationResolvers['createPost'] = ({ input }) => {
|
|
||||||
return db.post.create({
|
|
||||||
data: input,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
|
|
||||||
return db.post.update({
|
|
||||||
data: input,
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
|
|
||||||
return db.post.delete({
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
}
|
|
@ -16,7 +16,6 @@
|
|||||||
"@redwoodjs/router": "8.3.0",
|
"@redwoodjs/router": "8.3.0",
|
||||||
"@redwoodjs/web": "8.3.0",
|
"@redwoodjs/web": "8.3.0",
|
||||||
"@redwoodjs/web-server": "8.3.0",
|
"@redwoodjs/web-server": "8.3.0",
|
||||||
"humanize-string": "2.1.0",
|
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
|
@ -7,23 +7,13 @@
|
|||||||
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
||||||
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
||||||
|
|
||||||
import { Set, Router, Route, PrivateSet } from '@redwoodjs/router'
|
import { Router, Route } from '@redwoodjs/router'
|
||||||
|
|
||||||
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
|
|
||||||
|
|
||||||
import { useAuth } from './auth'
|
import { useAuth } from './auth'
|
||||||
|
|
||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
<Router useAuth={useAuth}>
|
<Router useAuth={useAuth}>
|
||||||
<PrivateSet unauthenticated="home">
|
|
||||||
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
|
|
||||||
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
|
|
||||||
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
|
|
||||||
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
|
|
||||||
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
|
|
||||||
</Set>
|
|
||||||
</PrivateSet>
|
|
||||||
<Route path="/login" page={LoginPage} name="login" />
|
<Route path="/login" page={LoginPage} name="login" />
|
||||||
<Route path="/signup" page={SignupPage} name="signup" />
|
<Route path="/signup" page={SignupPage} name="signup" />
|
||||||
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
import type {
|
|
||||||
EditPostById,
|
|
||||||
UpdatePostInput,
|
|
||||||
UpdatePostMutationVariables,
|
|
||||||
} from 'types/graphql'
|
|
||||||
|
|
||||||
import { navigate, routes } from '@redwoodjs/router'
|
|
||||||
import type {
|
|
||||||
CellSuccessProps,
|
|
||||||
CellFailureProps,
|
|
||||||
TypedDocumentNode,
|
|
||||||
} from '@redwoodjs/web'
|
|
||||||
import { useMutation } from '@redwoodjs/web'
|
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
|
||||||
|
|
||||||
import PostForm from 'src/components/Post/PostForm'
|
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<EditPostById> = gql`
|
|
||||||
query EditPostById($id: Int!) {
|
|
||||||
post: post(id: $id) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
body
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const UPDATE_POST_MUTATION: TypedDocumentNode<
|
|
||||||
EditPostById,
|
|
||||||
UpdatePostMutationVariables
|
|
||||||
> = gql`
|
|
||||||
mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
|
|
||||||
updatePost(id: $id, input: $input) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
body
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const Loading = () => <div>Loading...</div>
|
|
||||||
|
|
||||||
export const Failure = ({ error }: CellFailureProps) => (
|
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Success = ({ post }: CellSuccessProps<EditPostById>) => {
|
|
||||||
const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
|
|
||||||
onCompleted: () => {
|
|
||||||
toast.success('Post updated')
|
|
||||||
navigate(routes.posts())
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSave = (input: UpdatePostInput, id: EditPostById['post']['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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import type {
|
|
||||||
CreatePostMutation,
|
|
||||||
CreatePostInput,
|
|
||||||
CreatePostMutationVariables,
|
|
||||||
} from 'types/graphql'
|
|
||||||
|
|
||||||
import { navigate, routes } from '@redwoodjs/router'
|
|
||||||
import { useMutation } from '@redwoodjs/web'
|
|
||||||
import type { TypedDocumentNode } from '@redwoodjs/web'
|
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
|
||||||
|
|
||||||
import PostForm from 'src/components/Post/PostForm'
|
|
||||||
|
|
||||||
const CREATE_POST_MUTATION: TypedDocumentNode<
|
|
||||||
CreatePostMutation,
|
|
||||||
CreatePostMutationVariables
|
|
||||||
> = gql`
|
|
||||||
mutation CreatePostMutation($input: CreatePostInput!) {
|
|
||||||
createPost(input: $input) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const NewPost = () => {
|
|
||||||
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
|
|
||||||
onCompleted: () => {
|
|
||||||
toast.success('Post created')
|
|
||||||
navigate(routes.posts())
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSave = (input: CreatePostInput) => {
|
|
||||||
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
|
|
@ -1,98 +0,0 @@
|
|||||||
import type {
|
|
||||||
DeletePostMutation,
|
|
||||||
DeletePostMutationVariables,
|
|
||||||
FindPostById,
|
|
||||||
} from 'types/graphql'
|
|
||||||
|
|
||||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
|
||||||
import { useMutation } from '@redwoodjs/web'
|
|
||||||
import type { TypedDocumentNode } from '@redwoodjs/web'
|
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
|
||||||
|
|
||||||
import { timeTag } from 'src/lib/formatters'
|
|
||||||
|
|
||||||
const DELETE_POST_MUTATION: TypedDocumentNode<
|
|
||||||
DeletePostMutation,
|
|
||||||
DeletePostMutationVariables
|
|
||||||
> = gql`
|
|
||||||
mutation DeletePostMutation($id: Int!) {
|
|
||||||
deletePost(id: $id) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
post: NonNullable<FindPostById['post']>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Post = ({ post }: Props) => {
|
|
||||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
|
||||||
onCompleted: () => {
|
|
||||||
toast.success('Post deleted')
|
|
||||||
navigate(routes.posts())
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onDeleteClick = (id: DeletePostMutationVariables['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>
|
|
||||||
<tr>
|
|
||||||
<th>Updated at</th>
|
|
||||||
<td>{timeTag(post.updatedAt)}</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>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rw-button rw-button-red"
|
|
||||||
onClick={() => onDeleteClick(post.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Post
|
|
@ -1,36 +0,0 @@
|
|||||||
import type { FindPostById, FindPostByIdVariables } from 'types/graphql'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CellSuccessProps,
|
|
||||||
CellFailureProps,
|
|
||||||
TypedDocumentNode,
|
|
||||||
} from '@redwoodjs/web'
|
|
||||||
|
|
||||||
import Post from 'src/components/Post/Post'
|
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<FindPostById, FindPostByIdVariables> =
|
|
||||||
gql`
|
|
||||||
query FindPostById($id: Int!) {
|
|
||||||
post: post(id: $id) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
body
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const Loading = () => <div>Loading...</div>
|
|
||||||
|
|
||||||
export const Empty = () => <div>Post not found</div>
|
|
||||||
|
|
||||||
export const Failure = ({ error }: CellFailureProps<FindPostByIdVariables>) => (
|
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Success = ({
|
|
||||||
post,
|
|
||||||
}: CellSuccessProps<FindPostById, FindPostByIdVariables>) => {
|
|
||||||
return <Post post={post} />
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
import type { EditPostById, UpdatePostInput } from 'types/graphql'
|
|
||||||
|
|
||||||
import type { RWGqlError } from '@redwoodjs/forms'
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormError,
|
|
||||||
FieldError,
|
|
||||||
Label,
|
|
||||||
TextField,
|
|
||||||
Submit,
|
|
||||||
} from '@redwoodjs/forms'
|
|
||||||
|
|
||||||
type FormPost = NonNullable<EditPostById['post']>
|
|
||||||
|
|
||||||
interface PostFormProps {
|
|
||||||
post?: EditPostById['post']
|
|
||||||
onSave: (data: UpdatePostInput, id?: FormPost['id']) => void
|
|
||||||
error: RWGqlError
|
|
||||||
loading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const PostForm = (props: PostFormProps) => {
|
|
||||||
const onSubmit = (data: FormPost) => {
|
|
||||||
props.onSave(data, props?.post?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rw-form-wrapper">
|
|
||||||
<Form<FormPost> 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
|
|
@ -1,102 +0,0 @@
|
|||||||
import type {
|
|
||||||
DeletePostMutation,
|
|
||||||
DeletePostMutationVariables,
|
|
||||||
FindPosts,
|
|
||||||
} from 'types/graphql'
|
|
||||||
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import { useMutation } from '@redwoodjs/web'
|
|
||||||
import type { TypedDocumentNode } from '@redwoodjs/web'
|
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
|
||||||
|
|
||||||
import { QUERY } from 'src/components/Post/PostsCell'
|
|
||||||
import { timeTag, truncate } from 'src/lib/formatters'
|
|
||||||
|
|
||||||
const DELETE_POST_MUTATION: TypedDocumentNode<
|
|
||||||
DeletePostMutation,
|
|
||||||
DeletePostMutationVariables
|
|
||||||
> = gql`
|
|
||||||
mutation DeletePostMutation($id: Int!) {
|
|
||||||
deletePost(id: $id) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PostsList = ({ posts }: FindPosts) => {
|
|
||||||
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
|
|
||||||
onCompleted: () => {
|
|
||||||
toast.success('Post deleted')
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message)
|
|
||||||
},
|
|
||||||
// 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: DeletePostMutationVariables['id']) => {
|
|
||||||
if (confirm('Are you sure you want to delete post ' + id + '?')) {
|
|
||||||
deletePost({ variables: { id } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>Updated 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>{timeTag(post.updatedAt)}</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>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={'Delete post ' + post.id}
|
|
||||||
className="rw-button rw-button-small rw-button-red"
|
|
||||||
onClick={() => onDeleteClick(post.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostsList
|
|
@ -1,45 +0,0 @@
|
|||||||
import type { FindPosts, FindPostsVariables } from 'types/graphql'
|
|
||||||
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import type {
|
|
||||||
CellSuccessProps,
|
|
||||||
CellFailureProps,
|
|
||||||
TypedDocumentNode,
|
|
||||||
} from '@redwoodjs/web'
|
|
||||||
|
|
||||||
import Posts from 'src/components/Post/Posts'
|
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<FindPosts, FindPostsVariables> = gql`
|
|
||||||
query FindPosts {
|
|
||||||
posts {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
body
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
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 Failure = ({ error }: CellFailureProps<FindPosts>) => (
|
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Success = ({
|
|
||||||
posts,
|
|
||||||
}: CellSuccessProps<FindPosts, FindPostsVariables>) => {
|
|
||||||
return <Posts posts={posts} />
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import { Toaster } from '@redwoodjs/web/toast'
|
|
||||||
|
|
||||||
type LayoutProps = {
|
|
||||||
title: string
|
|
||||||
titleTo: keyof typeof routes
|
|
||||||
buttonLabel: string
|
|
||||||
buttonTo: keyof typeof routes
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScaffoldLayout = ({
|
|
||||||
title,
|
|
||||||
titleTo,
|
|
||||||
buttonLabel,
|
|
||||||
buttonTo,
|
|
||||||
children,
|
|
||||||
}: LayoutProps) => {
|
|
||||||
return (
|
|
||||||
<div className="rw-scaffold">
|
|
||||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
|
||||||
<header className="rw-header">
|
|
||||||
<h1 className="rw-heading rw-heading-primary">
|
|
||||||
<Link to={routes[titleTo]()} className="rw-link">
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
</h1>
|
|
||||||
<Link to={routes[buttonTo]()} className="rw-button rw-button-green">
|
|
||||||
<div className="rw-button-icon">+</div> {buttonLabel}
|
|
||||||
</Link>
|
|
||||||
</header>
|
|
||||||
<main className="rw-main">{children}</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScaffoldLayout
|
|
@ -1,192 +0,0 @@
|
|||||||
import { render, waitFor, screen } from '@redwoodjs/testing/web'
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatEnum,
|
|
||||||
jsonTruncate,
|
|
||||||
truncate,
|
|
||||||
timeTag,
|
|
||||||
jsonDisplay,
|
|
||||||
checkboxInputTag,
|
|
||||||
} from './formatters'
|
|
||||||
|
|
||||||
describe('formatEnum', () => {
|
|
||||||
it('handles nullish values', () => {
|
|
||||||
expect(formatEnum(null)).toEqual('')
|
|
||||||
expect(formatEnum('')).toEqual('')
|
|
||||||
expect(formatEnum(undefined)).toEqual('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('formats a list of values', () => {
|
|
||||||
expect(
|
|
||||||
formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET'])
|
|
||||||
).toEqual('Red, Orange, Yellow, Green, Blue, Violet')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('formats a single value', () => {
|
|
||||||
expect(formatEnum('DARK_BLUE')).toEqual('Dark blue')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns an empty string for values of the wrong type (for JS projects)', () => {
|
|
||||||
// @ts-expect-error - Testing JS scenario
|
|
||||||
expect(formatEnum(5)).toEqual('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('truncate', () => {
|
|
||||||
it('truncates really long strings', () => {
|
|
||||||
expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000)
|
|
||||||
expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not modify short strings', () => {
|
|
||||||
expect(truncate('Short strinG')).toEqual('Short strinG')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds ... to the end of truncated strings', () => {
|
|
||||||
expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('accepts numbers', () => {
|
|
||||||
expect(truncate(123)).toEqual('123')
|
|
||||||
expect(truncate(0)).toEqual('0')
|
|
||||||
expect(truncate(0o000)).toEqual('0')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('handles arguments of invalid type', () => {
|
|
||||||
// @ts-expect-error - Testing JS scenario
|
|
||||||
expect(truncate(false)).toEqual('false')
|
|
||||||
|
|
||||||
expect(truncate(undefined)).toEqual('')
|
|
||||||
expect(truncate(null)).toEqual('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('jsonTruncate', () => {
|
|
||||||
it('truncates large json structures', () => {
|
|
||||||
expect(
|
|
||||||
jsonTruncate({
|
|
||||||
foo: 'foo',
|
|
||||||
bar: 'bar',
|
|
||||||
baz: 'baz',
|
|
||||||
kittens: 'kittens meow',
|
|
||||||
bazinga: 'Sheldon',
|
|
||||||
nested: {
|
|
||||||
foobar: 'I have no imagination',
|
|
||||||
two: 'Second nested item',
|
|
||||||
},
|
|
||||||
five: 5,
|
|
||||||
bool: false,
|
|
||||||
})
|
|
||||||
).toMatch(/.+\n.+\w\.\.\.$/s)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('timeTag', () => {
|
|
||||||
it('renders a date', async () => {
|
|
||||||
render(<div>{timeTag(new Date('1970-08-20').toUTCString())}</div>)
|
|
||||||
|
|
||||||
await waitFor(() => screen.getByText(/1970.*00:00:00/))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can take an empty input string', async () => {
|
|
||||||
expect(timeTag('')).toEqual('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('jsonDisplay', () => {
|
|
||||||
it('produces the correct output', () => {
|
|
||||||
expect(
|
|
||||||
jsonDisplay({
|
|
||||||
title: 'TOML Example (but in JSON)',
|
|
||||||
database: {
|
|
||||||
data: [['delta', 'phi'], [3.14]],
|
|
||||||
enabled: true,
|
|
||||||
ports: [8000, 8001, 8002],
|
|
||||||
temp_targets: {
|
|
||||||
case: 72.0,
|
|
||||||
cpu: 79.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
dob: '1979-05-27T07:32:00-08:00',
|
|
||||||
name: 'Tom Preston-Werner',
|
|
||||||
},
|
|
||||||
servers: {
|
|
||||||
alpha: {
|
|
||||||
ip: '10.0.0.1',
|
|
||||||
role: 'frontend',
|
|
||||||
},
|
|
||||||
beta: {
|
|
||||||
ip: '10.0.0.2',
|
|
||||||
role: 'backend',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
<pre>
|
|
||||||
<code>
|
|
||||||
{
|
|
||||||
"title": "TOML Example (but in JSON)",
|
|
||||||
"database": {
|
|
||||||
"data": [
|
|
||||||
[
|
|
||||||
"delta",
|
|
||||||
"phi"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.14
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"enabled": true,
|
|
||||||
"ports": [
|
|
||||||
8000,
|
|
||||||
8001,
|
|
||||||
8002
|
|
||||||
],
|
|
||||||
"temp_targets": {
|
|
||||||
"case": 72,
|
|
||||||
"cpu": 79.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"owner": {
|
|
||||||
"dob": "1979-05-27T07:32:00-08:00",
|
|
||||||
"name": "Tom Preston-Werner"
|
|
||||||
},
|
|
||||||
"servers": {
|
|
||||||
"alpha": {
|
|
||||||
"ip": "10.0.0.1",
|
|
||||||
"role": "frontend"
|
|
||||||
},
|
|
||||||
"beta": {
|
|
||||||
"ip": "10.0.0.2",
|
|
||||||
"role": "backend"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('checkboxInputTag', () => {
|
|
||||||
it('can be checked', () => {
|
|
||||||
render(checkboxInputTag(true))
|
|
||||||
expect(screen.getByRole('checkbox')).toBeChecked()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can be unchecked', () => {
|
|
||||||
render(checkboxInputTag(false))
|
|
||||||
expect(screen.getByRole('checkbox')).not.toBeChecked()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is disabled when checked', () => {
|
|
||||||
render(checkboxInputTag(true))
|
|
||||||
expect(screen.getByRole('checkbox')).toBeDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is disabled when unchecked', () => {
|
|
||||||
render(checkboxInputTag(false))
|
|
||||||
expect(screen.getByRole('checkbox')).toBeDisabled()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,58 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import humanize from 'humanize-string'
|
|
||||||
|
|
||||||
const MAX_STRING_LENGTH = 150
|
|
||||||
|
|
||||||
export const formatEnum = (values: string | string[] | null | undefined) => {
|
|
||||||
let output = ''
|
|
||||||
|
|
||||||
if (Array.isArray(values)) {
|
|
||||||
const humanizedValues = values.map((value) => humanize(value))
|
|
||||||
output = humanizedValues.join(', ')
|
|
||||||
} else if (typeof values === 'string') {
|
|
||||||
output = humanize(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
export const jsonDisplay = (obj: unknown) => {
|
|
||||||
return (
|
|
||||||
<pre>
|
|
||||||
<code>{JSON.stringify(obj, null, 2)}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const truncate = (value: string | number) => {
|
|
||||||
let output = value?.toString() ?? ''
|
|
||||||
|
|
||||||
if (output.length > MAX_STRING_LENGTH) {
|
|
||||||
output = output.substring(0, MAX_STRING_LENGTH) + '...'
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
export const jsonTruncate = (obj: unknown) => {
|
|
||||||
return truncate(JSON.stringify(obj, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const timeTag = (dateTime?: string) => {
|
|
||||||
let output: string | JSX.Element = ''
|
|
||||||
|
|
||||||
if (dateTime) {
|
|
||||||
output = (
|
|
||||||
<time dateTime={dateTime} title={dateTime}>
|
|
||||||
{new Date(dateTime).toUTCString()}
|
|
||||||
</time>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkboxInputTag = (checked: boolean) => {
|
|
||||||
return <input type="checkbox" checked={checked} disabled />
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import EditPostCell from 'src/components/Post/EditPostCell'
|
|
||||||
|
|
||||||
type PostPageProps = {
|
|
||||||
id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditPostPage = ({ id }: PostPageProps) => {
|
|
||||||
return <EditPostCell id={id} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditPostPage
|
|
@ -1,7 +0,0 @@
|
|||||||
import NewPost from 'src/components/Post/NewPost'
|
|
||||||
|
|
||||||
const NewPostPage = () => {
|
|
||||||
return <NewPost />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewPostPage
|
|
@ -1,11 +0,0 @@
|
|||||||
import PostCell from 'src/components/Post/PostCell'
|
|
||||||
|
|
||||||
type PostPageProps = {
|
|
||||||
id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const PostPage = ({ id }: PostPageProps) => {
|
|
||||||
return <PostCell id={id} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostPage
|
|
@ -1,7 +0,0 @@
|
|||||||
import PostsCell from 'src/components/Post/PostsCell'
|
|
||||||
|
|
||||||
const PostsPage = () => {
|
|
||||||
return <PostsCell />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostsPage
|
|
@ -17108,7 +17108,6 @@ __metadata:
|
|||||||
"@redwoodjs/web-server": "npm:8.3.0"
|
"@redwoodjs/web-server": "npm:8.3.0"
|
||||||
"@types/react": "npm:^18.2.55"
|
"@types/react": "npm:^18.2.55"
|
||||||
"@types/react-dom": "npm:^18.2.19"
|
"@types/react-dom": "npm:^18.2.19"
|
||||||
humanize-string: "npm:2.1.0"
|
|
||||||
react: "npm:18.3.1"
|
react: "npm:18.3.1"
|
||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
Loading…
x
Reference in New Issue
Block a user