mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-25 10:03:07 +03:00
Updated todoApp example with one that uses filters, buttons and actions.
This commit is contained in:
parent
e45d077f54
commit
7c0e21b995
@ -1 +1 @@
|
||||
Generated on 2020-02-14 13:11:20.518192 UTC by waspc version "0.1.0.0" .
|
||||
Generated on 2020-03-10 18:22:45.230191331 UTC by waspc version "0.1.0.0" .
|
1719
examples/todoApp/out/package-lock.json
generated
1719
examples/todoApp/out/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"react-scripts": "3.4.0",
|
||||
"redux": "^4.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
|
@ -1 +0,0 @@
|
||||
.aboutPage { color: black; text-align: center; }
|
@ -1,37 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Todo from "./ext-src/Todo.js"
|
||||
|
||||
import * as taskState from './entities/task/state.js'
|
||||
import * as taskActions from './entities/task/actions.js'
|
||||
import Task from './entities/task/Task.js'
|
||||
import NewTaskForm from './entities/task/components/NewTaskForm.js'
|
||||
import TaskList from './entities/task/components/TaskList.js'
|
||||
|
||||
import './About.css'
|
||||
|
||||
|
||||
export class About extends Component {
|
||||
// TODO: Add propTypes.
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="aboutPage">
|
||||
<h1> About </h1>
|
||||
<p>This page was built with <a href="https://wasp-lang.dev">Wasp</a>!</p>
|
||||
<p> Check out source code
|
||||
<a href="https://github.com/wasp-lang/wasp/tree/master/examples/todoApp">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add,
|
||||
updateTask: taskActions.update,
|
||||
removeTask: taskActions.remove
|
||||
})(About)
|
30
examples/todoApp/out/src/components/DeleteDoneButton.js
Normal file
30
examples/todoApp/out/src/components/DeleteDoneButton.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Button from '@material-ui/core/Button'
|
||||
|
||||
import { deleteDoneAction } from '../entities/task/actions.js'
|
||||
|
||||
export class DeleteDoneButton extends React.Component {
|
||||
// TODO: Add propTypes.
|
||||
|
||||
onClick = () => {
|
||||
this.props.deleteDoneAction()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button {...this.props}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
Delete completed
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
// Selectors
|
||||
}), {
|
||||
// Actions
|
||||
deleteDoneAction
|
||||
})(DeleteDoneButton)
|
30
examples/todoApp/out/src/components/ToggleIsDoneButton.js
Normal file
30
examples/todoApp/out/src/components/ToggleIsDoneButton.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Button from '@material-ui/core/Button'
|
||||
|
||||
import { toggleIsDoneAction } from '../entities/task/actions.js'
|
||||
|
||||
export class ToggleIsDoneButton extends React.Component {
|
||||
// TODO: Add propTypes.
|
||||
|
||||
onClick = () => {
|
||||
this.props.toggleIsDoneAction()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button {...this.props}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
✓
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
// Selectors
|
||||
}), {
|
||||
// Actions
|
||||
toggleIsDoneAction
|
||||
})(ToggleIsDoneButton)
|
@ -1,3 +1,4 @@
|
||||
export const ADD = 'entities/task/ADD'
|
||||
export const SET = 'entities/task/SET'
|
||||
export const UPDATE = 'entities/task/UPDATE'
|
||||
export const REMOVE = 'entities/task/REMOVE'
|
||||
|
@ -1,4 +1,6 @@
|
||||
import * as types from './actionTypes'
|
||||
import Task from './Task'
|
||||
import { selectors } from './state'
|
||||
|
||||
|
||||
/**
|
||||
@ -9,6 +11,14 @@ export const add = (task) => ({
|
||||
data: task.toData()
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {Task[]} tasks
|
||||
*/
|
||||
export const set = (tasks) => ({
|
||||
type: types.SET,
|
||||
tasks: tasks.map(t => t.toData())
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {String} id
|
||||
* @param {Object} data - Partial data that will be merged with existing task.
|
||||
@ -26,3 +36,21 @@ export const remove = (id) => ({
|
||||
type: types.REMOVE,
|
||||
id
|
||||
})
|
||||
|
||||
export const toggleIsDoneAction = () => (dispatch, getState) => {
|
||||
const tasks = selectors.all(getState())
|
||||
const updateFn = tasks => {
|
||||
const areAllDone = tasks.every(t => t.isDone)
|
||||
return tasks.map(t => ({ ...t, isDone: !areAllDone }))
|
||||
}
|
||||
const newTasks = updateFn(tasks.map(t => t.toData())).map(t => new Task(t))
|
||||
dispatch(set(newTasks))
|
||||
}
|
||||
|
||||
export const deleteDoneAction = () => (dispatch, getState) => {
|
||||
const tasks = selectors.all(getState())
|
||||
const updateFn = tasks => tasks.filter(t => !t.isDone)
|
||||
const newTasks = updateFn(tasks.map(t => t.toData())).map(t => new Task(t))
|
||||
dispatch(set(newTasks))
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ import TableRow from '@material-ui/core/TableRow'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
|
||||
import * as taskState from '../state'
|
||||
import * as taskActions from '../actions'
|
||||
@ -21,12 +23,13 @@ import Task from '../Task'
|
||||
|
||||
export class TaskList extends React.Component {
|
||||
static propTypes = {
|
||||
editable: PropTypes.bool,
|
||||
filter: PropTypes.func
|
||||
editable: PropTypes.bool
|
||||
}
|
||||
|
||||
state = {
|
||||
taskBeingEdited: null
|
||||
taskBeingEdited: null,
|
||||
|
||||
filterName: 'all'
|
||||
}
|
||||
|
||||
setAsBeingEdited = task => this.setState({
|
||||
@ -44,13 +47,34 @@ export class TaskList extends React.Component {
|
||||
renderTaskDescription =
|
||||
(task) => task.isDone ? <s>{task.description}</s> : task.description
|
||||
|
||||
handleFilterChange = event => {
|
||||
this.setState({ filterName: event.target.value })
|
||||
}
|
||||
|
||||
filters = {
|
||||
'completed': task => task.isDone,
|
||||
'active': task => !task.isDone,
|
||||
}
|
||||
|
||||
render() {
|
||||
const taskListToShow = this.props.filter ?
|
||||
this.props.taskList.filter(this.props.filter) :
|
||||
const taskListToShow = this.state.filterName !== 'all' ?
|
||||
this.props.taskList.filter(this.filters[this.state.filterName]) :
|
||||
this.props.taskList
|
||||
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
|
||||
Filter:
|
||||
<Select
|
||||
value={this.state.filterName}
|
||||
onChange={this.handleFilterChange}
|
||||
>
|
||||
<MenuItem value="all">all</MenuItem>
|
||||
<MenuItem value="completed">completed</MenuItem>
|
||||
<MenuItem value="active">active</MenuItem>
|
||||
</Select>
|
||||
|
||||
<Paper>
|
||||
<Table>
|
||||
<TableHead style={{display: 'none'}}>
|
||||
@ -76,7 +100,8 @@ export class TaskList extends React.Component {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<ClickAwayListener onClickAway={() => this.finishEditing(task) }>
|
||||
<ClickAwayListener
|
||||
onClickAway={() => this.finishEditing(task) }>
|
||||
<TableCell
|
||||
onDoubleClick={() => this.setAsBeingEdited(task)}
|
||||
>
|
||||
|
@ -20,6 +20,12 @@ const reducer = (state = initialState, action) => {
|
||||
all: [ ...state.all, action.data ]
|
||||
}
|
||||
|
||||
case types.SET:
|
||||
return {
|
||||
...state,
|
||||
all: action.tasks
|
||||
}
|
||||
|
||||
case types.UPDATE:
|
||||
return {
|
||||
...state,
|
||||
|
@ -14,6 +14,9 @@ button.selected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.todos h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -35,7 +38,7 @@ button.selected {
|
||||
background-color: white;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-size: 24px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -55,17 +58,3 @@ button.selected {
|
||||
.todos__footer__filters .filter:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mainPage__nav {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mainPage__nav__aboutLink {
|
||||
color: lightgray;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mainPage__nav__aboutLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -7,53 +7,20 @@ import { connect } from 'react-redux'
|
||||
// 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 Task from '../entities/task/Task'
|
||||
import NewTaskForm from '../entities/task/components/NewTaskForm'
|
||||
import TaskList from '../entities/task/components/TaskList'
|
||||
import * as taskState from '../entities/task/state.js'
|
||||
import * as taskActions from '../entities/task/actions.js'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
import ToggleIsDoneButton from '../components/ToggleIsDoneButton'
|
||||
import DeleteDoneButton from '../components/DeleteDoneButton'
|
||||
|
||||
class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
this.props.taskList.map(t => this.props.updateTask(t.id, { isDone: !areAllDone }))
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={'filter ' + (this.state.taskFilterName === filterType ? 'selected' : '')}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
@ -61,12 +28,10 @@ class Todo extends React.Component {
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<button
|
||||
<ToggleIsDoneButton
|
||||
disabled={!this.isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
onClick={this.toggleIsDoneForAllTasks}>
|
||||
✓
|
||||
</button>
|
||||
/>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
@ -76,35 +41,21 @@ class Todo extends React.Component {
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
<TaskList editable />
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__filters">
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
<DeleteDoneButton
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -113,7 +64,5 @@ class Todo extends React.Component {
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add,
|
||||
updateTask: taskActions.update,
|
||||
removeTask: taskActions.remove
|
||||
addTask: taskActions.add
|
||||
})(Todo)
|
||||
|
@ -14,6 +14,9 @@ button.selected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.todos h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -35,7 +38,7 @@ button.selected {
|
||||
background-color: white;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-size: 24px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -55,17 +58,3 @@ button.selected {
|
||||
.todos__footer__filters .filter:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mainPage__nav {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mainPage__nav__aboutLink {
|
||||
color: lightgray;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mainPage__nav__aboutLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -7,53 +7,20 @@ import { connect } from 'react-redux'
|
||||
// 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 Task from '@wasp/entities/task/Task'
|
||||
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'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
import ToggleIsDoneButton from '@wasp/components/ToggleIsDoneButton'
|
||||
import DeleteDoneButton from '@wasp/components/DeleteDoneButton'
|
||||
|
||||
class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
this.props.taskList.map(t => this.props.updateTask(t.id, { isDone: !areAllDone }))
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={'filter ' + (this.state.taskFilterName === filterType ? 'selected' : '')}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
@ -61,12 +28,10 @@ class Todo extends React.Component {
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<button
|
||||
<ToggleIsDoneButton
|
||||
disabled={!this.isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
onClick={this.toggleIsDoneForAllTasks}>
|
||||
✓
|
||||
</button>
|
||||
/>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
@ -76,35 +41,21 @@ class Todo extends React.Component {
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
<TaskList editable />
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__filters">
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
<DeleteDoneButton
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -113,7 +64,5 @@ class Todo extends React.Component {
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add,
|
||||
updateTask: taskActions.update,
|
||||
removeTask: taskActions.remove
|
||||
addTask: taskActions.add
|
||||
})(Todo)
|
||||
|
@ -46,5 +46,30 @@ entity-list<Task> TaskList {
|
||||
// - 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=}
|
||||
|
Loading…
Reference in New Issue
Block a user