mirror of
https://github.com/wasp-lang/wasp.git
synced 2025-01-07 01:46:57 +03:00
Added Button and Action<entity>, also updated Todo app to use them.
This commit is contained in:
parent
fd39e5b658
commit
e45d077f54
39
waspc/data/Generator/templates/react-app/src/components/_Button.js
vendored
Normal file
39
waspc/data/Generator/templates/react-app/src/components/_Button.js
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{{={= =}=}}
|
||||||
|
import React from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import Button from '@material-ui/core/Button'
|
||||||
|
|
||||||
|
{=# onClickAction =}
|
||||||
|
import { {= exportedIdentifier =} } from '{= importPath =}'
|
||||||
|
{=/ onClickAction =}
|
||||||
|
|
||||||
|
export class {= button.name =} extends React.Component {
|
||||||
|
// TODO: Add propTypes.
|
||||||
|
|
||||||
|
{=# onClickAction =}
|
||||||
|
onClick = () => {
|
||||||
|
this.props.{= exportedIdentifier =}()
|
||||||
|
}
|
||||||
|
{=/ onClickAction =}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Button {...this.props}
|
||||||
|
{=# onClickAction =}
|
||||||
|
onClick={this.onClick}
|
||||||
|
{=/ onClickAction =}
|
||||||
|
>
|
||||||
|
{= button.label =}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => ({
|
||||||
|
// Selectors
|
||||||
|
}), {
|
||||||
|
// Actions
|
||||||
|
{=# onClickAction =}
|
||||||
|
{= exportedIdentifier =}
|
||||||
|
{=/ onClickAction =}
|
||||||
|
})({= button.name =})
|
@ -1,7 +1,7 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import uuidv4 from 'uuid/v4'
|
import uuidv4 from 'uuid/v4'
|
||||||
|
|
||||||
export default class {= entity.name =} {
|
export default class {= entityClassName =} {
|
||||||
_data = {}
|
_data = {}
|
||||||
|
|
||||||
constructor (data = {}) {
|
constructor (data = {}) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
export const ADD = 'entities/{= entityLowerName =}/ADD'
|
export const ADD = 'entities/{= entityLowerName =}/ADD'
|
||||||
|
export const SET = 'entities/{= entityLowerName =}/SET'
|
||||||
export const UPDATE = 'entities/{= entityLowerName =}/UPDATE'
|
export const UPDATE = 'entities/{= entityLowerName =}/UPDATE'
|
||||||
export const REMOVE = 'entities/{= entityLowerName =}/REMOVE'
|
export const REMOVE = 'entities/{= entityLowerName =}/REMOVE'
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import * as types from './actionTypes'
|
import * as types from './actionTypes'
|
||||||
|
import {=entityClassName=} from './{=entityClassName=}'
|
||||||
|
import { selectors } from './state'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{= entity.name =}} {= entityLowerName =}
|
* @param {{= entity.name =}} {= entityLowerName =}
|
||||||
*/
|
*/
|
||||||
export const add = ({= entityLowerName =}) => ({
|
export const add = ({=_entity=}) => ({
|
||||||
type: types.ADD,
|
type: types.ADD,
|
||||||
data: {= entityLowerName =}.toData()
|
data: {=_entity=}.toData()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{=entityClassName=}[]} {=_entities=}
|
||||||
|
*/
|
||||||
|
export const set = ({=_entities=}) => ({
|
||||||
|
type: types.SET,
|
||||||
|
{=_entities=}: {=_entities=}.map({=_e=} => {=_e=}.toData())
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,3 +37,13 @@ export const remove = (id) => ({
|
|||||||
type: types.REMOVE,
|
type: types.REMOVE,
|
||||||
id
|
id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
{=# entityActions =}
|
||||||
|
export const {=name=} = () => (dispatch, getState) => {
|
||||||
|
const {=_entities=} = selectors.all(getState())
|
||||||
|
const updateFn = {=&updateFn=}
|
||||||
|
const new{=_Entities=} = updateFn({=_entities=}.map({=_e=} => {=_e=}.toData())).map({=_e=} => new {=entityClassName=}({=_e=}))
|
||||||
|
dispatch(set(new{=_Entities=}))
|
||||||
|
}
|
||||||
|
|
||||||
|
{=/ entityActions =}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import {= entity.name =} from './{= entity.name =}'
|
import {=entityClassName=} from './{=entityClassName=}'
|
||||||
import * as types from './actionTypes'
|
import * as types from './actionTypes'
|
||||||
|
|
||||||
|
|
||||||
// We assume that root reducer of the app will put this reducer under
|
// We assume that root reducer of the app will put this reducer under
|
||||||
// key ROOT_REDUCER_KEY.
|
// key ROOT_REDUCER_KEY.
|
||||||
const ROOT_REDUCER_KEY = 'entities/{= entity.name =}'
|
const ROOT_REDUCER_KEY = 'entities/{=entity.name=}'
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
all: []
|
all: []
|
||||||
@ -21,14 +21,20 @@ const reducer = (state = initialState, action) => {
|
|||||||
all: [ ...state.all, action.data ]
|
all: [ ...state.all, action.data ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case types.SET:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
all: action.{=_entities=}
|
||||||
|
}
|
||||||
|
|
||||||
case types.UPDATE:
|
case types.UPDATE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
all: state.all.map(
|
all: state.all.map(
|
||||||
{= entityLowerName =} =>
|
{=_entity=} =>
|
||||||
{= entityLowerName =}.id === action.id
|
{=_entity=}.id === action.id
|
||||||
? { ...{= entityLowerName =}, ...action.data }
|
? { ...{=_entity=}, ...action.data }
|
||||||
: {= entityLowerName =}
|
: {=_entity=}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +42,7 @@ const reducer = (state = initialState, action) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
all: state.all.filter(
|
all: state.all.filter(
|
||||||
{= entityLowerName =} => {= entityLowerName =}.id !== action.id
|
{=_entity=} => {=_entity=}.id !== action.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +59,7 @@ selectors.root = (state) => state[ROOT_REDUCER_KEY]
|
|||||||
* @returns {{= entity.name =}[]}
|
* @returns {{= entity.name =}[]}
|
||||||
*/
|
*/
|
||||||
selectors.all = createSelector(selectors.root, (state) => {
|
selectors.all = createSelector(selectors.root, (state) => {
|
||||||
return state.all.map(data => new {= entity.name =}(data))
|
return state.all.map(data => new {=entityClassName=}(data))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ button.selected {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
border: none;
|
border: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 24px;
|
font-size: 24px !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,24 +7,16 @@ import { connect } from 'react-redux'
|
|||||||
// These will have well defined and documented APIs and paths.
|
// These will have well defined and documented APIs and paths.
|
||||||
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
||||||
// we made in todoApp.wasp file.
|
// we made in todoApp.wasp file.
|
||||||
import Task from '@wasp/entities/task/Task'
|
|
||||||
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
||||||
import TaskList from '@wasp/entities/task/components/TaskList'
|
import TaskList from '@wasp/entities/task/components/TaskList'
|
||||||
import * as taskState from '@wasp/entities/task/state.js'
|
import * as taskState from '@wasp/entities/task/state.js'
|
||||||
import * as taskActions from '@wasp/entities/task/actions.js'
|
import * as taskActions from '@wasp/entities/task/actions.js'
|
||||||
|
import ToggleIsDoneButton from '@wasp/components/ToggleIsDoneButton'
|
||||||
|
import DeleteDoneButton from '@wasp/components/DeleteDoneButton'
|
||||||
|
|
||||||
class Todo extends React.Component {
|
class Todo extends React.Component {
|
||||||
// TODO: prop types.
|
// TODO: prop types.
|
||||||
|
|
||||||
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)
|
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||||
|
|
||||||
isThereAnyTask = () => this.props.taskList.length > 0
|
isThereAnyTask = () => this.props.taskList.length > 0
|
||||||
@ -36,12 +28,10 @@ class Todo extends React.Component {
|
|||||||
<h1> Todos </h1>
|
<h1> Todos </h1>
|
||||||
|
|
||||||
<div className="todos__toggleAndInput">
|
<div className="todos__toggleAndInput">
|
||||||
<button
|
<ToggleIsDoneButton
|
||||||
disabled={!this.isThereAnyTask()}
|
disabled={!this.isThereAnyTask()}
|
||||||
className="todos__toggleButton"
|
className="todos__toggleButton"
|
||||||
onClick={this.toggleIsDoneForAllTasks}>
|
/>
|
||||||
✓
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<NewTaskForm
|
<NewTaskForm
|
||||||
className="todos__newTaskForm"
|
className="todos__newTaskForm"
|
||||||
@ -51,9 +41,7 @@ class Todo extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ this.isThereAnyTask() && (<>
|
{ this.isThereAnyTask() && (<>
|
||||||
<TaskList
|
<TaskList editable />
|
||||||
editable
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="todos__footer">
|
<div className="todos__footer">
|
||||||
<div className="todos__footer__itemsLeft">
|
<div className="todos__footer__itemsLeft">
|
||||||
@ -61,11 +49,9 @@ class Todo extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="todos__footer__clearCompleted">
|
<div className="todos__footer__clearCompleted">
|
||||||
<button
|
<DeleteDoneButton
|
||||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||||
onClick={this.deleteCompletedTasks}>
|
/>
|
||||||
Clear completed
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>)}
|
||||||
@ -78,7 +64,5 @@ class Todo extends React.Component {
|
|||||||
export default connect(state => ({
|
export default connect(state => ({
|
||||||
taskList: taskState.selectors.all(state)
|
taskList: taskState.selectors.all(state)
|
||||||
}), {
|
}), {
|
||||||
addTask: taskActions.add,
|
addTask: taskActions.add
|
||||||
updateTask: taskActions.update,
|
|
||||||
removeTask: taskActions.remove
|
|
||||||
})(Todo)
|
})(Todo)
|
||||||
|
@ -52,3 +52,24 @@ entity-list<Task> TaskList {
|
|||||||
active: {=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=}
|
||||||
|
58
waspc/src/Generator/Button.hs
Normal file
58
waspc/src/Generator/Button.hs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
module Generator.Button
|
||||||
|
( generateButtons
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
import Data.Aeson ((.=), object)
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified System.FilePath as FP
|
||||||
|
import Path ((</>), relfile, reldir)
|
||||||
|
import qualified Path
|
||||||
|
import qualified Path.Aliases as Path
|
||||||
|
import qualified Path.Extra as Path
|
||||||
|
|
||||||
|
import Wasp (Wasp)
|
||||||
|
import qualified Wasp
|
||||||
|
import qualified Wasp.Button as WButton
|
||||||
|
import Generator.FileDraft (FileDraft, createTemplateFileDraft)
|
||||||
|
import qualified Generator.Entity
|
||||||
|
import qualified Generator.Common as Common
|
||||||
|
|
||||||
|
generateButtons :: Wasp -> [FileDraft]
|
||||||
|
generateButtons wasp = concatMap (generateButton wasp) (Wasp.getButtons wasp)
|
||||||
|
|
||||||
|
generateButton :: Wasp -> WButton.Button -> [FileDraft]
|
||||||
|
generateButton wasp button =
|
||||||
|
[ generateButtonComponent wasp button
|
||||||
|
]
|
||||||
|
|
||||||
|
generateButtonComponent :: Wasp -> WButton.Button -> FileDraft
|
||||||
|
generateButtonComponent wasp button = createTemplateFileDraft dstPath srcPath (Just templateData)
|
||||||
|
where
|
||||||
|
srcPath = [reldir|src|] </> [reldir|components|] </> [relfile|_Button.js|]
|
||||||
|
dstPath = Common.srcDirPath </> buttonDirPathInSrc </> (fromJust $ Path.parseRelFile $ (WButton._name button) ++ ".js")
|
||||||
|
|
||||||
|
onClickActionData :: Maybe Aeson.Value
|
||||||
|
onClickActionData = do
|
||||||
|
actionName <- WButton._onClickActionName button
|
||||||
|
action <- Wasp.getActionByName wasp actionName
|
||||||
|
let (pathInSrc, exportedIdentifier) = Generator.Entity.getImportInfoForAction wasp action
|
||||||
|
return $ object [ "importPath" .= buildImportPathFromPathInSrc pathInSrc
|
||||||
|
, "exportedIdentifier" .= exportedIdentifier
|
||||||
|
]
|
||||||
|
|
||||||
|
templateData = object $
|
||||||
|
[ "wasp" .= wasp
|
||||||
|
, "button" .= button
|
||||||
|
]
|
||||||
|
++ maybe [] (\d -> ["onClickAction" .= d]) onClickActionData
|
||||||
|
|
||||||
|
buttonDirPathInSrc :: Path.RelDir
|
||||||
|
buttonDirPathInSrc = [reldir|components|]
|
||||||
|
|
||||||
|
-- | Takes path relative to the src path of generated project and turns it into relative path that can be
|
||||||
|
-- used as "from" part of the import in the button component source file.
|
||||||
|
-- NOTE: Here we return FilePath instead of Path because we need stuff like "./" or "../" in the path,
|
||||||
|
-- which Path would normalize away.
|
||||||
|
buildImportPathFromPathInSrc :: Path.Path Path.Rel a -> FilePath
|
||||||
|
buildImportPathFromPathInSrc pathInSrc = (Path.reversePath buttonDirPathInSrc) FP.</> (Path.toFilePath pathInSrc)
|
@ -6,6 +6,8 @@ module Generator.Entity
|
|||||||
, entityStatePathInSrc
|
, entityStatePathInSrc
|
||||||
, entityActionsPathInSrc
|
, entityActionsPathInSrc
|
||||||
|
|
||||||
|
, getImportInfoForAction
|
||||||
|
|
||||||
-- EXPORTED FOR TESTING:
|
-- EXPORTED FOR TESTING:
|
||||||
, generateEntityClass
|
, generateEntityClass
|
||||||
, generateEntityState
|
, generateEntityState
|
||||||
@ -16,12 +18,15 @@ module Generator.Entity
|
|||||||
, entityTemplatesDirPath
|
, entityTemplatesDirPath
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Path ((</>), relfile)
|
import Path ((</>), relfile)
|
||||||
import qualified Path
|
import qualified Path
|
||||||
import qualified Path.Aliases as Path
|
import qualified Path.Aliases as Path
|
||||||
|
import Util (jsonSet)
|
||||||
|
|
||||||
import Wasp
|
import Wasp
|
||||||
|
import qualified Wasp.Action
|
||||||
import Generator.FileDraft
|
import Generator.FileDraft
|
||||||
import qualified Generator.Common as Common
|
import qualified Generator.Common as Common
|
||||||
import Generator.Entity.EntityForm (generateEntityCreateForm)
|
import Generator.Entity.EntityForm (generateEntityCreateForm)
|
||||||
@ -59,7 +64,23 @@ generateEntityActionTypes wasp entity
|
|||||||
|
|
||||||
generateEntityActions :: Wasp -> Entity -> FileDraft
|
generateEntityActions :: Wasp -> Entity -> FileDraft
|
||||||
generateEntityActions wasp entity
|
generateEntityActions wasp entity
|
||||||
= createSimpleEntityFileDraft wasp entity (entityActionsPathInSrc entity) [relfile|actions.js|]
|
= createEntityFileDraft (entityActionsPathInSrc entity) [relfile|actions.js|] (Just templateData)
|
||||||
|
where
|
||||||
|
entityActions = getActionsForEntity wasp entity
|
||||||
|
templateData = jsonSet "entityActions" (Aeson.toJSON entityActions) (entityTemplateData wasp entity)
|
||||||
|
|
||||||
|
-- | Provides information on how to import and use given action.
|
||||||
|
-- Returns: (path (in src dir) to import action from, identifier under which it is exported).
|
||||||
|
-- NOTE: This function is in this module because this is where logic for generating action is,
|
||||||
|
-- but ideally that would move to more-standalone action generator and so would this function.
|
||||||
|
getImportInfoForAction :: Wasp -> Wasp.Action.Action -> (Path.RelFile, String)
|
||||||
|
getImportInfoForAction wasp action = (pathInSrc, exportedIdentifier)
|
||||||
|
where
|
||||||
|
-- NOTE: For now here we bravely assume that entity with such name exists.
|
||||||
|
Just entity = Wasp.getEntityByName wasp $ Wasp.Action._entityName action
|
||||||
|
pathInSrc = entityActionsPathInSrc entity
|
||||||
|
exportedIdentifier = Wasp.Action._name action
|
||||||
|
|
||||||
|
|
||||||
generateEntityComponents :: Wasp -> Entity -> [FileDraft]
|
generateEntityComponents :: Wasp -> Entity -> [FileDraft]
|
||||||
generateEntityComponents wasp entity = concat
|
generateEntityComponents wasp entity = concat
|
||||||
@ -82,11 +103,16 @@ generateEntityLists wasp entity = map (generateEntityList wasp) entityLists
|
|||||||
-- | Helper function that captures common logic for generating entity file draft.
|
-- | Helper function that captures common logic for generating entity file draft.
|
||||||
createSimpleEntityFileDraft :: Wasp -> Entity -> Path.RelFile -> Path.RelFile -> FileDraft
|
createSimpleEntityFileDraft :: Wasp -> Entity -> Path.RelFile -> Path.RelFile -> FileDraft
|
||||||
createSimpleEntityFileDraft wasp entity dstPathInSrc srcPathInEntityTemplatesDir
|
createSimpleEntityFileDraft wasp entity dstPathInSrc srcPathInEntityTemplatesDir
|
||||||
= createTemplateFileDraft dstPath srcPath (Just templateData)
|
= createEntityFileDraft dstPathInSrc srcPathInEntityTemplatesDir (Just templateData)
|
||||||
|
where
|
||||||
|
templateData = entityTemplateData wasp entity
|
||||||
|
|
||||||
|
createEntityFileDraft :: Path.RelFile -> Path.RelFile -> Maybe Aeson.Value -> FileDraft
|
||||||
|
createEntityFileDraft dstPathInSrc srcPathInEntityTemplatesDir maybeTemplateData =
|
||||||
|
createTemplateFileDraft dstPath srcPath maybeTemplateData
|
||||||
where
|
where
|
||||||
srcPath = entityTemplatesDirPath </> srcPathInEntityTemplatesDir
|
srcPath = entityTemplatesDirPath </> srcPathInEntityTemplatesDir
|
||||||
dstPath = Common.srcDirPath </> dstPathInSrc
|
dstPath = Common.srcDirPath </> dstPathInSrc
|
||||||
templateData = entityTemplateData wasp entity
|
|
||||||
|
|
||||||
-- * Paths of generated code (relative to src/ directory)
|
-- * Paths of generated code (relative to src/ directory)
|
||||||
|
|
||||||
|
@ -39,16 +39,28 @@ entityTemplateData wasp entity = object
|
|||||||
[ "wasp" .= wasp
|
[ "wasp" .= wasp
|
||||||
, "entity" .= entity
|
, "entity" .= entity
|
||||||
, "entityLowerName" .= getEntityLowerName entity
|
, "entityLowerName" .= getEntityLowerName entity
|
||||||
|
, "entityUpperName" .= getEntityUpperName entity
|
||||||
-- TODO: use it also when creating Class file itself and in other files.
|
-- TODO: use it also when creating Class file itself and in other files.
|
||||||
, "entityClassName" .= getEntityClassName entity
|
, "entityClassName" .= getEntityClassName entity
|
||||||
, "entityTypedFields" .= map entityFieldToJsonWithTypeAsKey (entityFields entity)
|
, "entityTypedFields" .= map entityFieldToJsonWithTypeAsKey (entityFields entity)
|
||||||
|
-- Below are shorthands, so that templates are more readable.
|
||||||
|
-- Each one has comment example for Task entity.
|
||||||
|
, "_entity" .= getEntityLowerName entity -- task
|
||||||
|
, "_entities" .= ((getEntityLowerName entity) ++ "s") -- tasks
|
||||||
|
, "_Entity" .= getEntityUpperName entity -- Task
|
||||||
|
, "_Entities" .= ((getEntityUpperName entity) ++ "s") -- Tasks
|
||||||
|
, "_e" .= [head $ getEntityLowerName entity] -- t
|
||||||
|
, "_es" .= ((head $ getEntityLowerName entity) : "s") -- ts
|
||||||
]
|
]
|
||||||
|
|
||||||
getEntityLowerName :: Entity -> String
|
getEntityLowerName :: Entity -> String
|
||||||
getEntityLowerName = Util.toLowerFirst . entityName
|
getEntityLowerName = Util.toLowerFirst . entityName
|
||||||
|
|
||||||
|
getEntityUpperName :: Entity -> String
|
||||||
|
getEntityUpperName = Util.toUpperFirst . entityName
|
||||||
|
|
||||||
getEntityClassName :: Entity -> String
|
getEntityClassName :: Entity -> String
|
||||||
getEntityClassName = Util.toUpperFirst . entityName
|
getEntityClassName = getEntityUpperName
|
||||||
|
|
||||||
{- | Converts entity field to a JSON where field type is a key set to true, along with
|
{- | Converts entity field to a JSON where field type is a key set to true, along with
|
||||||
all other field properties.
|
all other field properties.
|
||||||
|
@ -14,6 +14,7 @@ import Generator.FileDraft
|
|||||||
import qualified Generator.Entity as EntityGenerator
|
import qualified Generator.Entity as EntityGenerator
|
||||||
import qualified Generator.PageGenerator as PageGenerator
|
import qualified Generator.PageGenerator as PageGenerator
|
||||||
import qualified Generator.ExternalCode as ExternalCodeGenerator
|
import qualified Generator.ExternalCode as ExternalCodeGenerator
|
||||||
|
import qualified Generator.Button
|
||||||
import qualified Generator.Common as Common
|
import qualified Generator.Common as Common
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ generateSrcDir wasp
|
|||||||
]
|
]
|
||||||
++ PageGenerator.generatePages wasp
|
++ PageGenerator.generatePages wasp
|
||||||
++ EntityGenerator.generateEntities wasp
|
++ EntityGenerator.generateEntities wasp
|
||||||
|
++ Generator.Button.generateButtons wasp
|
||||||
++ [generateReducersJs wasp]
|
++ [generateReducersJs wasp]
|
||||||
|
|
||||||
generateReducersJs :: Wasp -> FileDraft
|
generateReducersJs :: Wasp -> FileDraft
|
||||||
|
@ -19,6 +19,12 @@ reservedNameApp = "app"
|
|||||||
reservedNamePage :: String
|
reservedNamePage :: String
|
||||||
reservedNamePage = "page"
|
reservedNamePage = "page"
|
||||||
|
|
||||||
|
reservedNameButton :: String
|
||||||
|
reservedNameButton = "button"
|
||||||
|
|
||||||
|
reservedNameAction :: String
|
||||||
|
reservedNameAction = "action"
|
||||||
|
|
||||||
reservedNameEntity :: String
|
reservedNameEntity :: String
|
||||||
reservedNameEntity = "entity"
|
reservedNameEntity = "entity"
|
||||||
|
|
||||||
@ -51,6 +57,8 @@ reservedNames =
|
|||||||
, reservedNamePage
|
, reservedNamePage
|
||||||
, reservedNameEntity
|
, reservedNameEntity
|
||||||
, reservedNameEntityForm
|
, reservedNameEntityForm
|
||||||
|
, reservedNameButton
|
||||||
|
, reservedNameAction
|
||||||
-- * Data types
|
-- * Data types
|
||||||
, reservedNameString
|
, reservedNameString
|
||||||
, reservedNameBoolean
|
, reservedNameBoolean
|
||||||
|
@ -15,6 +15,8 @@ import Parser.Entity (entity)
|
|||||||
import Parser.Entity.EntityForm (entityForm)
|
import Parser.Entity.EntityForm (entityForm)
|
||||||
import Parser.Entity.EntityList (entityList)
|
import Parser.Entity.EntityList (entityList)
|
||||||
import Parser.JsImport (jsImport)
|
import Parser.JsImport (jsImport)
|
||||||
|
import Parser.Button (button)
|
||||||
|
import Parser.Action (action)
|
||||||
import Parser.Common (runWaspParser)
|
import Parser.Common (runWaspParser)
|
||||||
|
|
||||||
|
|
||||||
@ -25,10 +27,18 @@ waspElement
|
|||||||
<|> waspElementEntity
|
<|> waspElementEntity
|
||||||
<|> waspElementEntityForm
|
<|> waspElementEntityForm
|
||||||
<|> waspElementEntityList
|
<|> waspElementEntityList
|
||||||
|
<|> waspElementButton
|
||||||
|
<|> waspElementAction
|
||||||
|
|
||||||
waspElementApp :: Parser Wasp.WaspElement
|
waspElementApp :: Parser Wasp.WaspElement
|
||||||
waspElementApp = Wasp.WaspElementApp <$> app
|
waspElementApp = Wasp.WaspElementApp <$> app
|
||||||
|
|
||||||
|
waspElementButton :: Parser Wasp.WaspElement
|
||||||
|
waspElementButton = Wasp.WaspElementButton <$> button
|
||||||
|
|
||||||
|
waspElementAction :: Parser Wasp.WaspElement
|
||||||
|
waspElementAction = Wasp.WaspElementAction <$> action
|
||||||
|
|
||||||
waspElementPage :: Parser Wasp.WaspElement
|
waspElementPage :: Parser Wasp.WaspElement
|
||||||
waspElementPage = Wasp.WaspElementPage <$> page
|
waspElementPage = Wasp.WaspElementPage <$> page
|
||||||
|
|
||||||
|
21
waspc/src/Parser/Action.hs
Normal file
21
waspc/src/Parser/Action.hs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module Parser.Action
|
||||||
|
( action
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Text.Parsec.String (Parser)
|
||||||
|
|
||||||
|
import qualified Wasp.Action as Action
|
||||||
|
|
||||||
|
import qualified Parser.Common as Common
|
||||||
|
import qualified Parser.JsCode as JsCode
|
||||||
|
import qualified Lexer as L
|
||||||
|
|
||||||
|
|
||||||
|
action :: Parser Action.Action
|
||||||
|
action = do
|
||||||
|
(entityName, actionName, updateFn) <- Common.waspElementLinkedToEntity L.reservedNameAction JsCode.jsCode
|
||||||
|
return Action.Action
|
||||||
|
{ Action._name = actionName
|
||||||
|
, Action._entityName = entityName
|
||||||
|
, Action._updateFn = updateFn
|
||||||
|
}
|
44
waspc/src/Parser/Button.hs
Normal file
44
waspc/src/Parser/Button.hs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
module Parser.Button
|
||||||
|
( button
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Text.Parsec
|
||||||
|
import Text.Parsec.String (Parser)
|
||||||
|
|
||||||
|
import qualified Lexer as L
|
||||||
|
import qualified Wasp.Button as Button
|
||||||
|
import Parser.Common
|
||||||
|
|
||||||
|
data Property
|
||||||
|
= Label !String
|
||||||
|
| OnClick !String -- ^ Name of action to execute on click.
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
-- | Parses Button properties, separated by a comma.
|
||||||
|
properties :: Parser [Property]
|
||||||
|
properties = L.commaSep1 $
|
||||||
|
propLabel
|
||||||
|
<|> propOnClickActionName
|
||||||
|
|
||||||
|
propLabel :: Parser Property
|
||||||
|
propLabel = Label <$> waspPropertyStringLiteral "label"
|
||||||
|
|
||||||
|
propOnClickActionName :: Parser Property
|
||||||
|
propOnClickActionName = OnClick <$> waspProperty "onClick" L.identifier
|
||||||
|
|
||||||
|
getLabel :: [Property] -> String
|
||||||
|
getLabel ps = head $ [c | Label c <- ps]
|
||||||
|
|
||||||
|
getOnClickActionName :: [Property] -> Maybe String
|
||||||
|
getOnClickActionName ps = let actions = [a | OnClick a <- ps]
|
||||||
|
in if null actions then Nothing else Just (head actions)
|
||||||
|
|
||||||
|
button :: Parser Button.Button
|
||||||
|
button = do
|
||||||
|
(buttonName, buttonProps) <- waspElementNameAndClosure L.reservedNameButton properties
|
||||||
|
|
||||||
|
return Button.Button
|
||||||
|
{ Button._name = buttonName
|
||||||
|
, Button._label = getLabel buttonProps
|
||||||
|
, Button._onClickActionName = getOnClickActionName buttonProps
|
||||||
|
}
|
@ -50,18 +50,17 @@ waspElementNameAndClosure elementType closure =
|
|||||||
return (elementName, closureContent)
|
return (elementName, closureContent)
|
||||||
|
|
||||||
-- | Parses declaration of a wasp element linked to an entity.
|
-- | Parses declaration of a wasp element linked to an entity.
|
||||||
-- E.g. "entity-form<Task> {...}" or "entity-list<Task> {...}"
|
-- E.g. "entity-form<Task> ..." or "action<Task> ..."
|
||||||
waspElementLinkedToEntity
|
waspElementLinkedToEntity
|
||||||
:: String -- ^ Type of the linked wasp element (e.g. "entity-form").
|
:: String -- ^ Type of the linked wasp element (e.g. "entity-form").
|
||||||
-> Parser a -- ^ Parser to be used for parsing closure content of the wasp element.
|
-> Parser a -- ^ Parser to be used for parsing body of the wasp element.
|
||||||
-> Parser (String, String, a) -- ^ Name of the linked entity, element name and closure content.
|
-> Parser (String, String, a) -- ^ Name of the linked entity, element name and body.
|
||||||
waspElementLinkedToEntity elementType closure = do
|
waspElementLinkedToEntity elementType bodyParser = do
|
||||||
L.reserved elementType
|
L.reserved elementType
|
||||||
linkedEntityName <- L.angles L.identifier
|
linkedEntityName <- L.angles L.identifier
|
||||||
elementName <- L.identifier
|
elementName <- L.identifier
|
||||||
closureContent <- waspClosure closure
|
body <- bodyParser
|
||||||
|
return (linkedEntityName, elementName, body)
|
||||||
return (linkedEntityName, elementName, closureContent)
|
|
||||||
|
|
||||||
-- | Parses wasp property along with the key, "key: value".
|
-- | Parses wasp property along with the key, "key: value".
|
||||||
waspProperty :: String -> Parser a -> Parser a
|
waspProperty :: String -> Parser a -> Parser a
|
||||||
|
@ -26,7 +26,7 @@ import qualified Lexer as L
|
|||||||
entityForm :: Parser EntityForm
|
entityForm :: Parser EntityForm
|
||||||
entityForm = do
|
entityForm = do
|
||||||
(entityName, formName, options) <-
|
(entityName, formName, options) <-
|
||||||
P.waspElementLinkedToEntity L.reservedNameEntityForm entityFormOptions
|
P.waspElementLinkedToEntity L.reservedNameEntityForm (P.waspClosure entityFormOptions)
|
||||||
|
|
||||||
return WEF.EntityForm
|
return WEF.EntityForm
|
||||||
{ WEF._name = formName
|
{ WEF._name = formName
|
||||||
|
@ -22,7 +22,7 @@ import qualified Lexer as L
|
|||||||
entityList :: Parser EntityList
|
entityList :: Parser EntityList
|
||||||
entityList = do
|
entityList = do
|
||||||
(entityName, listName, options) <-
|
(entityName, listName, options) <-
|
||||||
P.waspElementLinkedToEntity L.reservedNameEntityList entityListOptions
|
P.waspElementLinkedToEntity L.reservedNameEntityList (P.waspClosure entityListOptions)
|
||||||
|
|
||||||
return WEL.EntityList
|
return WEL.EntityList
|
||||||
{ WEL._name = listName
|
{ WEL._name = listName
|
||||||
|
@ -20,6 +20,14 @@ module Wasp
|
|||||||
, getEntityFormsForEntity
|
, getEntityFormsForEntity
|
||||||
, getEntityListsForEntity
|
, getEntityListsForEntity
|
||||||
|
|
||||||
|
, getButtons
|
||||||
|
, addButton
|
||||||
|
|
||||||
|
, getActions
|
||||||
|
, addAction
|
||||||
|
, getActionsForEntity
|
||||||
|
, getActionByName
|
||||||
|
|
||||||
, module Wasp.Page
|
, module Wasp.Page
|
||||||
, getPages
|
, getPages
|
||||||
, addPage
|
, addPage
|
||||||
@ -37,6 +45,9 @@ import qualified Wasp.EntityForm as EF
|
|||||||
import qualified Wasp.EntityList as EL
|
import qualified Wasp.EntityList as EL
|
||||||
import Wasp.JsImport
|
import Wasp.JsImport
|
||||||
import Wasp.Page
|
import Wasp.Page
|
||||||
|
import Wasp.Button
|
||||||
|
import Wasp.Action (Action)
|
||||||
|
import qualified Wasp.Action
|
||||||
|
|
||||||
import qualified Util as U
|
import qualified Util as U
|
||||||
|
|
||||||
@ -54,6 +65,8 @@ data WaspElement
|
|||||||
| WaspElementEntity !Entity
|
| WaspElementEntity !Entity
|
||||||
| WaspElementEntityForm !EF.EntityForm
|
| WaspElementEntityForm !EF.EntityForm
|
||||||
| WaspElementEntityList !EL.EntityList
|
| WaspElementEntityList !EL.EntityList
|
||||||
|
| WaspElementButton !Button
|
||||||
|
| WaspElementAction !Action
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
fromWaspElems :: [WaspElement] -> Wasp
|
fromWaspElems :: [WaspElement] -> Wasp
|
||||||
@ -108,6 +121,33 @@ getPages wasp = [page | (WaspElementPage page) <- waspElements wasp]
|
|||||||
addPage :: Wasp -> Page -> Wasp
|
addPage :: Wasp -> Page -> Wasp
|
||||||
addPage wasp page = wasp { waspElements = (WaspElementPage page):(waspElements wasp) }
|
addPage wasp page = wasp { waspElements = (WaspElementPage page):(waspElements wasp) }
|
||||||
|
|
||||||
|
-- * Button
|
||||||
|
|
||||||
|
getButtons :: Wasp -> [Button]
|
||||||
|
getButtons wasp = [button | (WaspElementButton button) <- waspElements wasp]
|
||||||
|
|
||||||
|
addButton :: Wasp -> Button -> Wasp
|
||||||
|
addButton wasp button = wasp { waspElements = (WaspElementButton button):(waspElements wasp) }
|
||||||
|
|
||||||
|
-- * Action
|
||||||
|
|
||||||
|
getActions :: Wasp -> [Action]
|
||||||
|
getActions wasp = [action | (WaspElementAction action) <- waspElements wasp]
|
||||||
|
|
||||||
|
addAction :: Wasp -> Action -> Wasp
|
||||||
|
addAction wasp action = wasp { waspElements = (WaspElementAction action):(waspElements wasp) }
|
||||||
|
|
||||||
|
-- | Gets action with a specified name from wasp, if such an action exists.
|
||||||
|
-- We assume here that there are no two actions with same name.
|
||||||
|
getActionByName :: Wasp -> String -> Maybe Action
|
||||||
|
getActionByName wasp name = U.headSafe $ filter (\a -> Wasp.Action._name a == name) (getActions wasp)
|
||||||
|
|
||||||
|
-- | Retrieves all actions that are performed on specific entity.
|
||||||
|
getActionsForEntity :: Wasp -> Entity -> [Action]
|
||||||
|
getActionsForEntity wasp entity = filter isActionOfGivenEntity (getActions wasp)
|
||||||
|
where
|
||||||
|
isActionOfGivenEntity action = entityName entity == Wasp.Action._entityName action
|
||||||
|
|
||||||
-- * Entities
|
-- * Entities
|
||||||
|
|
||||||
getEntities :: Wasp -> [Entity]
|
getEntities :: Wasp -> [Entity]
|
||||||
|
26
waspc/src/Wasp/Action.hs
Normal file
26
waspc/src/Wasp/Action.hs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module Wasp.Action
|
||||||
|
( Action(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson ((.=), object, ToJSON(..))
|
||||||
|
import Wasp.JsCode (JsCode)
|
||||||
|
|
||||||
|
-- Although name is general (Action), we are actually making many implicit assumptions here:
|
||||||
|
-- That Action is an update action that updates the whole collection of certain entity at once.
|
||||||
|
-- We are doing this because it is general enough for our purposes right now, but in the future we
|
||||||
|
-- will want to have much more developed system of actions, and Action will probably only be the
|
||||||
|
-- umbrella name/type for all of the action types.
|
||||||
|
|
||||||
|
data Action = Action
|
||||||
|
{ _name :: !String
|
||||||
|
, _entityName :: !String
|
||||||
|
-- | Js is expected to be function that takes list of entities and returns new list of entities.
|
||||||
|
, _updateFn :: !JsCode
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance ToJSON Action where
|
||||||
|
toJSON action = object
|
||||||
|
[ "name" .= _name action
|
||||||
|
, "entityName" .= _entityName action
|
||||||
|
, "updateFn" .= _updateFn action
|
||||||
|
]
|
18
waspc/src/Wasp/Button.hs
Normal file
18
waspc/src/Wasp/Button.hs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module Wasp.Button
|
||||||
|
( Button(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson ((.=), object, ToJSON(..))
|
||||||
|
|
||||||
|
data Button = Button
|
||||||
|
{ _name :: !String
|
||||||
|
, _label :: !String
|
||||||
|
, _onClickActionName :: !(Maybe String)
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance ToJSON Button where
|
||||||
|
toJSON button = object
|
||||||
|
[ "name" .= _name button
|
||||||
|
, "label" .= _label button
|
||||||
|
, "onClickActionName" .= _onClickActionName button
|
||||||
|
]
|
@ -13,7 +13,7 @@ spec_parseWaspCommon :: Spec
|
|||||||
spec_parseWaspCommon = do
|
spec_parseWaspCommon = do
|
||||||
describe "Parsing wasp element linked to an entity" $ do
|
describe "Parsing wasp element linked to an entity" $ do
|
||||||
it "When given a valid declaration, parses it correctly." $ do
|
it "When given a valid declaration, parses it correctly." $ do
|
||||||
runWaspParser (waspElementLinkedToEntity "entity-form" whiteSpace) "entity-form<Task> TaskForm { }"
|
runWaspParser (waspElementLinkedToEntity "entity-form" (waspClosure whiteSpace)) "entity-form<Task> TaskForm { }"
|
||||||
`shouldBe` Right ("Task", "TaskForm", ())
|
`shouldBe` Right ("Task", "TaskForm", ())
|
||||||
|
|
||||||
describe "Parsing wasp element name and properties" $ do
|
describe "Parsing wasp element name and properties" $ do
|
||||||
|
Loading…
Reference in New Issue
Block a user