mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-24 03:35:17 +03:00
Added react-query, implemented useQuery wrapper for it, refactored Todo
example app to use new queries, removed old Wasp code from it.
This commit is contained in:
parent
f5fe865193
commit
faf34a7d26
@ -10,6 +10,7 @@
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-query": "^2.14.1",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
const config = {
|
||||
apiUrl: 'https://localhost:3001'
|
||||
apiUrl: 'http://localhost:3001'
|
||||
}
|
||||
|
||||
export default config
|
||||
|
@ -3,9 +3,9 @@ import axios from 'axios'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const {= queryFnName =} = async ({ args, context }) => {
|
||||
const {= queryFnName =} = async (args) => {
|
||||
try {
|
||||
const response = await axios.post(config.apiUrl + '/{= queryRoute =}', { args })
|
||||
const response = await axios.post(config.apiUrl + '/{= queryRoute =}', args)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
// TODO: This is a really crude error handling for now, and we should look into improving it,
|
||||
@ -18,4 +18,6 @@ const {= queryFnName =} = async ({ args, context }) => {
|
||||
}
|
||||
}
|
||||
|
||||
{= queryFnName =}.useQueryKey = '{= queryRoute =}'
|
||||
|
||||
export default {= queryFnName =}
|
||||
|
18
waspc/data/Generator/templates/react-app/src/queries/index.js
vendored
Normal file
18
waspc/data/Generator/templates/react-app/src/queries/index.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import { useQuery as rqUseQuery } from 'react-query'
|
||||
|
||||
export const useQuery = (queryFn, queryFnArgs, config) => {
|
||||
if (typeof queryFn !== 'function') {
|
||||
throw new Error('useQuery requires queryFn to be a function.')
|
||||
}
|
||||
if (!queryFn.useQueryKey) {
|
||||
throw new Error('queryFn needs to have useQueryKey property defined.')
|
||||
}
|
||||
|
||||
const rqResult = rqUseQuery({
|
||||
queryKey: [queryFn.useQueryKey, queryFnArgs],
|
||||
queryFn: (_key, args) => queryFn(args),
|
||||
config
|
||||
})
|
||||
|
||||
return rqResult
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"morgan": "~1.9.1"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
|
||||
import indexRouter from './routes/index.js'
|
||||
|
||||
@ -9,6 +10,7 @@ import indexRouter from './routes/index.js'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
|
@ -17,7 +17,9 @@ export default handleRejection(async (req, res) => {
|
||||
// JSON objects or smth.
|
||||
// So for now we are just going with POST that has JSON in the body -> generated code is not
|
||||
// as human-like as it should be though.
|
||||
const result = await {= queryJsFnIdentifier =}({ args: req.body || {}, context: {} })
|
||||
res.json({ result })
|
||||
const args = req.body || {}
|
||||
const context = {}
|
||||
const result = await {= queryJsFnIdentifier =}(args, context)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
|
@ -1,68 +1,94 @@
|
||||
// As seen here, we can import npm packages used in code generated by Wasp, which will be clearly defined.
|
||||
// In the future, we will of course also be able to specify additional packages as dependencies.
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
// As seen here, we can import specific components/code generated by Wasp.
|
||||
// These will have well defined and documented APIs and paths.
|
||||
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
||||
// we made in todoApp.wasp file.
|
||||
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
||||
import TaskList from '@wasp/entities/task/components/TaskList'
|
||||
import * as taskState from '@wasp/entities/task/state.js'
|
||||
import * as taskActions from '@wasp/entities/task/actions.js'
|
||||
import ToggleIsDoneButton from '@wasp/components/ToggleIsDoneButton'
|
||||
import DeleteDoneButton from '@wasp/components/DeleteDoneButton'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import createTask from '@wasp/queries/createTask.js'
|
||||
|
||||
class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
const Todo = (props) => {
|
||||
const defaultNewTaskDescription = ''
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
const [newTaskDescription, setNewTaskDescription] = useState(defaultNewTaskDescription)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
const { data: tasks, refetch, isFetching, isError, error: tasksError } = useQuery(getTasks)
|
||||
|
||||
const isAnyTaskCompleted = () => tasks?.some(t => t.isDone)
|
||||
|
||||
const isThereAnyTask = () => tasks?.length > 0
|
||||
|
||||
const createNewTask = async (description) => {
|
||||
const task = { isDone: false, description }
|
||||
await createTask(task)
|
||||
refetch()
|
||||
}
|
||||
|
||||
const handleNewTaskSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await createNewTask(newTaskDescription)
|
||||
setNewTaskDescription(defaultNewTaskDescription)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert('Error:' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const Tasks = (props) => {
|
||||
return <div>
|
||||
{ props.tasks.map((task, idx) => <Task task={task} key={idx}/>) }
|
||||
</div>
|
||||
}
|
||||
|
||||
const TasksError = (props) => {
|
||||
return 'Error during fetching tasks: ' + (tasksError?.message || '')
|
||||
}
|
||||
|
||||
const Task = (props) => {
|
||||
return <div>
|
||||
<input type="checkbox" checked={ props.task.isDone ? 'checked' : false } readOnly />
|
||||
<span> { props.task.description } </span>
|
||||
</div>
|
||||
}
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
<div className="todos__container">
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<ToggleIsDoneButton
|
||||
disabled={!this.isThereAnyTask()}
|
||||
<button
|
||||
disabled={!isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
/>
|
||||
> ✓ </button>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
<form onSubmit={handleNewTaskSubmit}>
|
||||
<input type="text"
|
||||
value={newTaskDescription}
|
||||
onChange={e => setNewTaskDescription(e.target.value)}
|
||||
/>
|
||||
<input type="submit" value="Create new task"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList editable />
|
||||
{ isFetching && 'Fetching tasks...'}
|
||||
|
||||
{ isError && <TasksError/> }
|
||||
|
||||
{ isThereAnyTask() && (<>
|
||||
<Tasks tasks={tasks}/>
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
{ tasks.filter(t => !t.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<DeleteDoneButton
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
/>
|
||||
<button className={isAnyTaskCompleted() ? '' : 'hidden' }> Delete completed </button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add
|
||||
})(Todo)
|
||||
export default Todo
|
||||
|
8
waspc/examples/todoApp/ext/actions.js
Normal file
8
waspc/examples/todoApp/ext/actions.js
Normal file
@ -0,0 +1,8 @@
|
||||
import state from './state.js'
|
||||
|
||||
export const createTask = (task, context) => {
|
||||
if (Math.random() < 0.5) {
|
||||
throw new Error('Failed to create task, random error!')
|
||||
}
|
||||
state.tasks = [...(state.tasks || []), task]
|
||||
}
|
@ -9,7 +9,9 @@ export default class Main extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Todo/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import state from './state.js'
|
||||
|
||||
export const getTasks = async ({ args, context }) => {
|
||||
return [] // TODO: This is dummy for now.
|
||||
export const getTasks = async (args, context) => {
|
||||
if (Math.random() < 0.5) {
|
||||
throw new Error('Random error: getting tasks failed.')
|
||||
}
|
||||
return state.tasks || []
|
||||
}
|
||||
|
6
waspc/examples/todoApp/ext/state.js
Normal file
6
waspc/examples/todoApp/ext/state.js
Normal file
@ -0,0 +1,6 @@
|
||||
// This is a super simple, dummy implementation of "global" state, to be used in our examples
|
||||
// until we can start using the database.
|
||||
|
||||
const state = {}
|
||||
|
||||
export default state
|
@ -1,10 +1,3 @@
|
||||
import Todo from "@ext/Todo.js" // Imports non-wasp code from external code dir (ext/).
|
||||
|
||||
entity Task {
|
||||
isDone :: boolean,
|
||||
description :: string
|
||||
}
|
||||
|
||||
app todoApp {
|
||||
title: "ToDo App"
|
||||
}
|
||||
@ -31,62 +24,11 @@ page Task {
|
||||
component: import Task from "@ext/pages/Task"
|
||||
}
|
||||
|
||||
entity-form<Task> NewTaskForm {
|
||||
fields: {
|
||||
description: {
|
||||
show: true,
|
||||
label: none,
|
||||
placeholder: "What needs to be done?"
|
||||
},
|
||||
isDone: {
|
||||
show: false,
|
||||
defaultValue: false
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
onEnter: true,
|
||||
button: { show: false }
|
||||
}
|
||||
}
|
||||
|
||||
entity-list<Task> TaskList {
|
||||
showHeader: false,
|
||||
fields: {
|
||||
description: {
|
||||
// The contract for render is that user must provide a function that:
|
||||
// - Receives a task as an input.
|
||||
// - Returns a React Node or something that can be rendered by JSX.
|
||||
// - Does not depend on any outer context.
|
||||
render: {=js (task) => task.isDone ? <s>{task.description}</s> : task.description js=}
|
||||
}
|
||||
},
|
||||
mutuallyExclusiveFilters: {
|
||||
completed: {=js task => task.isDone js=},
|
||||
active: {=js task => !task.isDone js=}
|
||||
}
|
||||
}
|
||||
|
||||
button ToggleIsDoneButton {
|
||||
label: "✓",
|
||||
onClick: toggleIsDoneAction
|
||||
}
|
||||
|
||||
button DeleteDoneButton {
|
||||
label: "Delete completed",
|
||||
onClick: deleteDoneAction
|
||||
}
|
||||
|
||||
action<Task> toggleIsDoneAction {=js
|
||||
tasks => {
|
||||
const areAllDone = tasks.every(t => t.isDone)
|
||||
return tasks.map(t => ({ ...t, isDone: !areAllDone }))
|
||||
}
|
||||
js=}
|
||||
|
||||
action<Task> deleteDoneAction {=js
|
||||
tasks => tasks.filter(t => !t.isDone)
|
||||
js=}
|
||||
|
||||
query getTasks {
|
||||
fn: import { getTasks } from "@ext/queries.js"
|
||||
}
|
||||
|
||||
// TODO: This should be an action, not query! But we are cheating like this until we add support for actions.
|
||||
query createTask {
|
||||
fn: import { createTask } from "@ext/actions.js"
|
||||
}
|
@ -17,6 +17,7 @@ import qualified Generator.WebAppGenerator.Common as C
|
||||
genQueries :: Wasp -> [FileDraft]
|
||||
genQueries wasp = concat
|
||||
[ map (genQuery wasp) (Wasp.getQueries wasp)
|
||||
, [C.makeSimpleTemplateFD (C.asTmplFile [P.relfile|src/queries/index.js|]) wasp]
|
||||
]
|
||||
|
||||
genQuery :: Wasp -> Wasp.Query.Query -> FileDraft
|
||||
|
Loading…
Reference in New Issue
Block a user