mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-17 18:47:30 +03:00
Progress. WIP.
This commit is contained in:
parent
601ac3185b
commit
d0044dd320
115
examples/realworld/ext/ArticleEditorPage.js
Normal file
115
examples/realworld/ext/ArticleEditorPage.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
|
||||
import useAuth from '@wasp/auth/useAuth.js'
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import createArticle from '@wasp/actions/createArticle'
|
||||
import updateArticle from '@wasp/actions/updateArticle'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getArticle from '@wasp/queries/getArticle'
|
||||
|
||||
import Navbar from './Navbar'
|
||||
|
||||
const ArticleEditorPage = (props) => {
|
||||
const { data: user, isError } = useAuth({ keepPreviousData: true })
|
||||
|
||||
// TODO: Here, as in some other places, it feels tricky to figure out what is happening regarding the state.
|
||||
// When is article null, when not, should I look into combination of article and articleId, then
|
||||
// there is this 'enabled' which I need on the other hand -> uff. And what if I get error? humpf!
|
||||
const articleId = parseInt(props.match.params.articleId)
|
||||
const { data: article, error: articleError } = useQuery(getArticle, { id: articleId }, { enabled: articleId })
|
||||
|
||||
// TODO: Instead of this logic here, I wish I could use ACL via Wasp and just
|
||||
// receive user via props instead of useAuth().
|
||||
if (!user || isError) {
|
||||
return <span> Please <Link to='/login'>log in</Link>. </span>
|
||||
}
|
||||
|
||||
return articleError
|
||||
? articleError.message || articleError
|
||||
: (
|
||||
<div>
|
||||
<Navbar />
|
||||
<ArticleEditor user={user} article={article} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ArticleEditor = (props) => {
|
||||
const user = props.user
|
||||
const article = props.article
|
||||
|
||||
const history = useHistory()
|
||||
|
||||
const [title, setTitle] = useState(article?.title || '')
|
||||
const [description, setDescription] = useState(article?.description || '')
|
||||
const [markdownContent, setMarkdownContent] = useState(article?.markdownContent || '')
|
||||
|
||||
const [submitError, setSubmitError] = useState(null)
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
setSubmitError(null)
|
||||
try {
|
||||
let articleId
|
||||
if (article?.id) {
|
||||
await updateArticle({
|
||||
id: article.id,
|
||||
title,
|
||||
description,
|
||||
markdownContent
|
||||
})
|
||||
articleId = article.id
|
||||
} else {
|
||||
const newArticle = await createArticle({
|
||||
title,
|
||||
description,
|
||||
markdownContent
|
||||
})
|
||||
articleId = newArticle.id
|
||||
}
|
||||
history.push(`/article/${articleId}`)
|
||||
} catch (err) {
|
||||
setSubmitError(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ submitError && (
|
||||
<p>
|
||||
{ submitError.message || submitError }
|
||||
</p>
|
||||
) }
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h2>Article title</h2>
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
/>
|
||||
|
||||
<h2>What's this article about?</h2>
|
||||
<input
|
||||
type='text'
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
|
||||
<h2>Markdown content</h2>
|
||||
<input
|
||||
type='text'
|
||||
value={markdownContent}
|
||||
onChange={e => setMarkdownContent(e.target.value)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<input type='submit' value='Publish Article' />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticleEditorPage
|
65
examples/realworld/ext/ArticleViewPage.js
Normal file
65
examples/realworld/ext/ArticleViewPage.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import useAuth from '@wasp/auth/useAuth.js'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
|
||||
import getArticle from '@wasp/queries/getArticle'
|
||||
import deleteArticle from '@wasp/actions/deleteArticle'
|
||||
|
||||
import Navbar from './Navbar'
|
||||
|
||||
const ArticleViewPage = (props) => {
|
||||
const history = useHistory()
|
||||
const { data: user } = useAuth({ keepPreviousData: true })
|
||||
|
||||
const articleId = parseInt(props.match.params.articleId)
|
||||
const { data: article } = useQuery(getArticle, { id: articleId })
|
||||
|
||||
// TODO: If there is no such article, we get null here under `article`,
|
||||
// and we don't handle that properly, we just return blank screen (return null).
|
||||
// How should we detect this and handle it?
|
||||
// Should we modify action to return error instead of null?
|
||||
// Or should we check for (!isLoading && !article)?
|
||||
// What do we even do in such situation?
|
||||
// Or maybe we should make it so that every operations returns an object always,
|
||||
// and that object contains article then -> then it is very clear if something got returned,
|
||||
// or if it is that initial null.
|
||||
|
||||
const isMyArticle = user?.id && (user?.id === article?.userId)
|
||||
|
||||
const handleEditArticle = () => {
|
||||
history.push(`/editor/${article.id}`)
|
||||
}
|
||||
|
||||
const handleDeleteArticle = async () => {
|
||||
if (!window.confirm('Are you sure you want to delete the article?')) return
|
||||
try {
|
||||
await deleteArticle({ id: article.id })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert('Failed to delete article: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
return article ? (
|
||||
<div>
|
||||
<Navbar />
|
||||
|
||||
<div>
|
||||
<p> { article.title } </p>
|
||||
<p> { article.description } </p>
|
||||
<p> { article.markdownContent } </p>
|
||||
</div>
|
||||
|
||||
{ isMyArticle && (
|
||||
<div>
|
||||
<button onClick={handleEditArticle}> Edit Article </button>
|
||||
<button onClick={handleDeleteArticle}> Delete Article </button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default ArticleViewPage
|
@ -11,6 +11,7 @@ const Navbar = () => {
|
||||
return (
|
||||
<div>
|
||||
<Link to='/'> Home </Link>
|
||||
<Link to='/editor'> New Article </Link>
|
||||
<Link to='/settings'> Settings </Link>
|
||||
<a href={`/@${user.username}`}> { user.username } </a>
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ import useAuth from '@wasp/auth/useAuth.js'
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import updateUser from '@wasp/actions/updateUser'
|
||||
import getUser from '@wasp/queries/getUser'
|
||||
import getArticlesByUser from '@wasp/queries/getArticlesByUser'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
|
||||
import Navbar from './Navbar'
|
||||
@ -36,10 +37,37 @@ const UserProfilePage = (props) => {
|
||||
<p> { user.username } </p>
|
||||
<p> { user.bio } </p>
|
||||
<div>
|
||||
{ /* TODO: Show this link only if user is logged in. */ }
|
||||
<Link to='/settings'>Edit Profile Settings</Link>
|
||||
</div>
|
||||
|
||||
<Articles user={user} />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
const Articles = (props) => {
|
||||
// TODO: Should I have pagination here, probably I should?
|
||||
|
||||
const { data: articles } = useQuery(getArticlesByUser, { username: props.user.username })
|
||||
|
||||
return articles ? (
|
||||
<div>
|
||||
{ articles.map(article => <Article article={article} key={article.id} />) }
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
const Article = (props) => {
|
||||
const article = props.article
|
||||
return (
|
||||
<div>
|
||||
<Link to={`/article/${article.id}`}>
|
||||
<h2> { article.title } </h2>
|
||||
</Link>
|
||||
<p> { article.description } </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfilePage
|
||||
|
@ -15,11 +15,14 @@ export const signup = async ({ username, email, password }, context) => {
|
||||
}
|
||||
|
||||
export const updateUser = async ({ email, username, bio, profilePictureUrl, newPassword }, context) => {
|
||||
if (!context.user) { throw new HttpError(403) }
|
||||
|
||||
// TODO: Nicer error handling! Right now everything is returned as 500 while it could be instead
|
||||
// useful error message about username being taken / not unique, and other validation errors.
|
||||
await context.entities.User.update({
|
||||
where: { email },
|
||||
where: { id: context.user.id },
|
||||
data: {
|
||||
email,
|
||||
username,
|
||||
bio,
|
||||
profilePictureUrl,
|
||||
@ -27,3 +30,43 @@ export const updateUser = async ({ email, username, bio, profilePictureUrl, newP
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createArticle = async ({ title, description, markdownContent }, context) => {
|
||||
if (!context.user) { throw new HttpError(403) }
|
||||
|
||||
// TODO: Nicer error handling! Right now everything is returned as 500 while it could be instead
|
||||
// useful error message about username being taken / not unique, and other validation errors.
|
||||
return await context.entities.Article.create({
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
markdownContent,
|
||||
user: { connect: { id: context.user.id } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const updateArticle = async ({ id, title, description, markdownContent }, context) => {
|
||||
if (!context.user) { throw new HttpError(403) }
|
||||
|
||||
// TODO: Nicer error handling! Right now everything is returned as 500 while it could be instead
|
||||
// useful error message about username being taken / not unique, and other validation errors.
|
||||
await context.entities.Article.updateMany({
|
||||
where: { id, user: { id: context.user.id }}, // TODO: This line is not fun to write.
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
markdownContent
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteArticle = async ({ id }, context) => {
|
||||
if (!context.user) { throw new HttpError(403) }
|
||||
|
||||
// TODO: Nicer error handling! Right now everything is returned as 500 while it could be instead
|
||||
// useful error message about username being taken / not unique, and other validation errors.
|
||||
await context.entities.Article.deleteMany({
|
||||
where: { id, user: { id: context.user.id }} // TODO: This line is not fun to write.
|
||||
})
|
||||
}
|
||||
|
@ -6,3 +6,21 @@ export const getUser = async ({ username }, context) => {
|
||||
if (!user) throw new HttpError(404, 'No user with username ' + username)
|
||||
return user
|
||||
}
|
||||
|
||||
export const getArticlesByUser = async ({ username }, context) => {
|
||||
// TODO: Do some error handling?
|
||||
const articles = await context.entities.Article.findMany({
|
||||
where: {
|
||||
user: { username }
|
||||
}
|
||||
})
|
||||
return articles
|
||||
}
|
||||
|
||||
export const getArticle = async ({ id }, context) => {
|
||||
// TODO: Do some error handling?
|
||||
const article = await context.entities.Article.findOne({
|
||||
where: { id }
|
||||
})
|
||||
return article
|
||||
}
|
||||
|
@ -27,6 +27,19 @@ page UserProfile {
|
||||
component: import UserProfile from "@ext/UserProfilePage.js"
|
||||
}
|
||||
|
||||
route "/editor/:articleId" -> page ArticleEditor
|
||||
page ArticleEditor {
|
||||
component: import ArticleEditor from "@ext/ArticleEditorPage.js"
|
||||
}
|
||||
|
||||
// TODO: Instead of articleId, we should some combination of its title and
|
||||
// a random string, if I got it right. Figure out better what is the spec
|
||||
// and implement it correctly.
|
||||
route "/article/:articleId" -> page ArticleView
|
||||
page ArticleView {
|
||||
component: import ArticleView from "@ext/ArticleViewPage.js"
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
@ -34,6 +47,18 @@ entity User {=psl
|
||||
password String
|
||||
bio String?
|
||||
profilePictureUrl String?
|
||||
articles Article[]
|
||||
psl=}
|
||||
|
||||
// TODO: Add tags.
|
||||
// TODO: Add creation and update times.
|
||||
entity Article {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String
|
||||
markdownContent String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
psl=}
|
||||
|
||||
auth {
|
||||
@ -54,4 +79,29 @@ action updateUser {
|
||||
query getUser {
|
||||
fn: import { getUser } from "@ext/queries.js",
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
query getArticlesByUser {
|
||||
fn: import { getArticlesByUser } from "@ext/queries.js",
|
||||
entities: [Article]
|
||||
}
|
||||
|
||||
query getArticle {
|
||||
fn: import { getArticle } from "@ext/queries.js",
|
||||
entities: [Article]
|
||||
}
|
||||
|
||||
action createArticle {
|
||||
fn: import { createArticle } from "@ext/actions.js",
|
||||
entities: [Article]
|
||||
}
|
||||
|
||||
action updateArticle {
|
||||
fn: import { updateArticle } from "@ext/actions.js",
|
||||
entities: [Article]
|
||||
}
|
||||
|
||||
action deleteArticle {
|
||||
fn: import { deleteArticle } from "@ext/actions.js",
|
||||
entities: [Article]
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
# Migration `20201119144622-added-article-entity`
|
||||
|
||||
This migration has been generated by Martin Sosic at 11/19/2020, 3:46:22 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Article" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"markdownContent" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration 20201117145436-added-bio-and-picture-to-user..20201119144622-added-article-entity
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,8 +1,8 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
- url = "***"
|
||||
+ url = "***"
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@@ -15,6 +15,16 @@
|
||||
email String @unique
|
||||
password String
|
||||
bio String?
|
||||
profilePictureUrl String?
|
||||
+ articles Article[]
|
||||
}
|
||||
+model Article {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ title String
|
||||
+ description String
|
||||
+ markdownContent String
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
+ userId Int
|
||||
+}
|
||||
+
|
||||
```
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
bio String?
|
||||
profilePictureUrl String?
|
||||
articles Article[]
|
||||
}
|
||||
|
||||
model Article {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String
|
||||
markdownContent String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Article"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "title",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "description",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "markdownContent",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "user",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[userId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Article",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Article",
|
||||
"field": "userId",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "articles",
|
||||
"type": "Article",
|
||||
"arity": "List"
|
||||
}
|
||||
]
|
||||
}
|
@ -2,4 +2,5 @@
|
||||
|
||||
20201030161549-user
|
||||
20201030185724-fixed-user
|
||||
20201117145436-added-bio-and-picture-to-user
|
||||
20201117145436-added-bio-and-picture-to-user
|
||||
20201119144622-added-article-entity
|
Loading…
Reference in New Issue
Block a user