Updated todoApp example with one that uses filters, buttons and actions.

This commit is contained in:
Martin Sosic 2020-03-10 19:30:15 +01:00
parent e45d077f54
commit 7c0e21b995
16 changed files with 1136 additions and 958 deletions

View File

@ -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" .

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View File

@ -1 +0,0 @@
.aboutPage { color: black; text-align: center; }

View File

@ -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 &nbsp;
<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)

View 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)

View 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)

View File

@ -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'

View File

@ -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))
}

View File

@ -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:&nbsp;
<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)}
>

View File

@ -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,

View File

@ -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;
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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)

View File

@ -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=}