mirror of
https://github.com/wasp-lang/wasp.git
synced 2025-01-05 08:15:34 +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'
|
||||
|
||||
export default class {= entity.name =} {
|
||||
export default class {= entityClassName =} {
|
||||
_data = {}
|
||||
|
||||
constructor (data = {}) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
{{={= =}=}}
|
||||
export const ADD = 'entities/{= entityLowerName =}/ADD'
|
||||
export const SET = 'entities/{= entityLowerName =}/SET'
|
||||
export const UPDATE = 'entities/{= entityLowerName =}/UPDATE'
|
||||
export const REMOVE = 'entities/{= entityLowerName =}/REMOVE'
|
||||
|
@ -1,13 +1,23 @@
|
||||
{{={= =}=}}
|
||||
import * as types from './actionTypes'
|
||||
import {=entityClassName=} from './{=entityClassName=}'
|
||||
import { selectors } from './state'
|
||||
|
||||
|
||||
/**
|
||||
* @param {{= entity.name =}} {= entityLowerName =}
|
||||
*/
|
||||
export const add = ({= entityLowerName =}) => ({
|
||||
export const add = ({=_entity=}) => ({
|
||||
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,
|
||||
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 {= entity.name =} from './{= entity.name =}'
|
||||
import {=entityClassName=} from './{=entityClassName=}'
|
||||
import * as types from './actionTypes'
|
||||
|
||||
|
||||
// We assume that root reducer of the app will put this reducer under
|
||||
// key ROOT_REDUCER_KEY.
|
||||
const ROOT_REDUCER_KEY = 'entities/{= entity.name =}'
|
||||
const ROOT_REDUCER_KEY = 'entities/{=entity.name=}'
|
||||
|
||||
const initialState = {
|
||||
all: []
|
||||
@ -21,14 +21,20 @@ const reducer = (state = initialState, action) => {
|
||||
all: [ ...state.all, action.data ]
|
||||
}
|
||||
|
||||
case types.SET:
|
||||
return {
|
||||
...state,
|
||||
all: action.{=_entities=}
|
||||
}
|
||||
|
||||
case types.UPDATE:
|
||||
return {
|
||||
...state,
|
||||
all: state.all.map(
|
||||
{= entityLowerName =} =>
|
||||
{= entityLowerName =}.id === action.id
|
||||
? { ...{= entityLowerName =}, ...action.data }
|
||||
: {= entityLowerName =}
|
||||
{=_entity=} =>
|
||||
{=_entity=}.id === action.id
|
||||
? { ...{=_entity=}, ...action.data }
|
||||
: {=_entity=}
|
||||
)
|
||||
}
|
||||
|
||||
@ -36,7 +42,7 @@ const reducer = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
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 =}[]}
|
||||
*/
|
||||
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;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-size: 24px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -7,24 +7,16 @@ 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'
|
||||
import ToggleIsDoneButton from '@wasp/components/ToggleIsDoneButton'
|
||||
import DeleteDoneButton from '@wasp/components/DeleteDoneButton'
|
||||
|
||||
class Todo extends React.Component {
|
||||
// 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)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
@ -36,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"
|
||||
@ -51,9 +41,7 @@ class Todo extends React.Component {
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
/>
|
||||
<TaskList editable />
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
@ -61,11 +49,9 @@ class Todo extends React.Component {
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
<DeleteDoneButton
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
@ -78,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)
|
||||
|
@ -52,3 +52,24 @@ entity-list<Task> TaskList {
|
||||
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
|
||||
, entityActionsPathInSrc
|
||||
|
||||
, getImportInfoForAction
|
||||
|
||||
-- EXPORTED FOR TESTING:
|
||||
, generateEntityClass
|
||||
, generateEntityState
|
||||
@ -16,12 +18,15 @@ module Generator.Entity
|
||||
, entityTemplatesDirPath
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Maybe (fromJust)
|
||||
import Path ((</>), relfile)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
import Util (jsonSet)
|
||||
|
||||
import Wasp
|
||||
import qualified Wasp.Action
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.Common as Common
|
||||
import Generator.Entity.EntityForm (generateEntityCreateForm)
|
||||
@ -59,7 +64,23 @@ generateEntityActionTypes wasp entity
|
||||
|
||||
generateEntityActions :: Wasp -> Entity -> FileDraft
|
||||
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 = concat
|
||||
@ -82,11 +103,16 @@ generateEntityLists wasp entity = map (generateEntityList wasp) entityLists
|
||||
-- | Helper function that captures common logic for generating entity file draft.
|
||||
createSimpleEntityFileDraft :: Wasp -> Entity -> Path.RelFile -> Path.RelFile -> FileDraft
|
||||
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
|
||||
srcPath = entityTemplatesDirPath </> srcPathInEntityTemplatesDir
|
||||
dstPath = Common.srcDirPath </> dstPathInSrc
|
||||
templateData = entityTemplateData wasp entity
|
||||
|
||||
-- * Paths of generated code (relative to src/ directory)
|
||||
|
||||
|
@ -39,16 +39,28 @@ entityTemplateData wasp entity = object
|
||||
[ "wasp" .= wasp
|
||||
, "entity" .= entity
|
||||
, "entityLowerName" .= getEntityLowerName entity
|
||||
, "entityUpperName" .= getEntityUpperName entity
|
||||
-- TODO: use it also when creating Class file itself and in other files.
|
||||
, "entityClassName" .= getEntityClassName 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 = Util.toLowerFirst . entityName
|
||||
|
||||
getEntityUpperName :: Entity -> String
|
||||
getEntityUpperName = Util.toUpperFirst . entityName
|
||||
|
||||
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
|
||||
all other field properties.
|
||||
|
@ -14,6 +14,7 @@ import Generator.FileDraft
|
||||
import qualified Generator.Entity as EntityGenerator
|
||||
import qualified Generator.PageGenerator as PageGenerator
|
||||
import qualified Generator.ExternalCode as ExternalCodeGenerator
|
||||
import qualified Generator.Button
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
|
||||
@ -63,6 +64,7 @@ generateSrcDir wasp
|
||||
]
|
||||
++ PageGenerator.generatePages wasp
|
||||
++ EntityGenerator.generateEntities wasp
|
||||
++ Generator.Button.generateButtons wasp
|
||||
++ [generateReducersJs wasp]
|
||||
|
||||
generateReducersJs :: Wasp -> FileDraft
|
||||
|
@ -19,6 +19,12 @@ reservedNameApp = "app"
|
||||
reservedNamePage :: String
|
||||
reservedNamePage = "page"
|
||||
|
||||
reservedNameButton :: String
|
||||
reservedNameButton = "button"
|
||||
|
||||
reservedNameAction :: String
|
||||
reservedNameAction = "action"
|
||||
|
||||
reservedNameEntity :: String
|
||||
reservedNameEntity = "entity"
|
||||
|
||||
@ -51,6 +57,8 @@ reservedNames =
|
||||
, reservedNamePage
|
||||
, reservedNameEntity
|
||||
, reservedNameEntityForm
|
||||
, reservedNameButton
|
||||
, reservedNameAction
|
||||
-- * Data types
|
||||
, reservedNameString
|
||||
, reservedNameBoolean
|
||||
|
@ -15,6 +15,8 @@ import Parser.Entity (entity)
|
||||
import Parser.Entity.EntityForm (entityForm)
|
||||
import Parser.Entity.EntityList (entityList)
|
||||
import Parser.JsImport (jsImport)
|
||||
import Parser.Button (button)
|
||||
import Parser.Action (action)
|
||||
import Parser.Common (runWaspParser)
|
||||
|
||||
|
||||
@ -25,10 +27,18 @@ waspElement
|
||||
<|> waspElementEntity
|
||||
<|> waspElementEntityForm
|
||||
<|> waspElementEntityList
|
||||
<|> waspElementButton
|
||||
<|> waspElementAction
|
||||
|
||||
waspElementApp :: Parser Wasp.WaspElement
|
||||
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 = 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)
|
||||
|
||||
-- | 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
|
||||
:: 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 (String, String, a) -- ^ Name of the linked entity, element name and closure content.
|
||||
waspElementLinkedToEntity elementType closure = do
|
||||
-> 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 body.
|
||||
waspElementLinkedToEntity elementType bodyParser = do
|
||||
L.reserved elementType
|
||||
linkedEntityName <- L.angles L.identifier
|
||||
elementName <- L.identifier
|
||||
closureContent <- waspClosure closure
|
||||
|
||||
return (linkedEntityName, elementName, closureContent)
|
||||
body <- bodyParser
|
||||
return (linkedEntityName, elementName, body)
|
||||
|
||||
-- | Parses wasp property along with the key, "key: value".
|
||||
waspProperty :: String -> Parser a -> Parser a
|
||||
|
@ -26,7 +26,7 @@ import qualified Lexer as L
|
||||
entityForm :: Parser EntityForm
|
||||
entityForm = do
|
||||
(entityName, formName, options) <-
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityForm entityFormOptions
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityForm (P.waspClosure entityFormOptions)
|
||||
|
||||
return WEF.EntityForm
|
||||
{ WEF._name = formName
|
||||
|
@ -22,7 +22,7 @@ import qualified Lexer as L
|
||||
entityList :: Parser EntityList
|
||||
entityList = do
|
||||
(entityName, listName, options) <-
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityList entityListOptions
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityList (P.waspClosure entityListOptions)
|
||||
|
||||
return WEL.EntityList
|
||||
{ WEL._name = listName
|
||||
|
@ -20,6 +20,14 @@ module Wasp
|
||||
, getEntityFormsForEntity
|
||||
, getEntityListsForEntity
|
||||
|
||||
, getButtons
|
||||
, addButton
|
||||
|
||||
, getActions
|
||||
, addAction
|
||||
, getActionsForEntity
|
||||
, getActionByName
|
||||
|
||||
, module Wasp.Page
|
||||
, getPages
|
||||
, addPage
|
||||
@ -37,6 +45,9 @@ import qualified Wasp.EntityForm as EF
|
||||
import qualified Wasp.EntityList as EL
|
||||
import Wasp.JsImport
|
||||
import Wasp.Page
|
||||
import Wasp.Button
|
||||
import Wasp.Action (Action)
|
||||
import qualified Wasp.Action
|
||||
|
||||
import qualified Util as U
|
||||
|
||||
@ -54,6 +65,8 @@ data WaspElement
|
||||
| WaspElementEntity !Entity
|
||||
| WaspElementEntityForm !EF.EntityForm
|
||||
| WaspElementEntityList !EL.EntityList
|
||||
| WaspElementButton !Button
|
||||
| WaspElementAction !Action
|
||||
deriving (Show, Eq)
|
||||
|
||||
fromWaspElems :: [WaspElement] -> Wasp
|
||||
@ -108,6 +121,33 @@ getPages wasp = [page | (WaspElementPage page) <- waspElements wasp]
|
||||
addPage :: Wasp -> Page -> 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
|
||||
|
||||
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
|
||||
describe "Parsing wasp element linked to an entity" $ 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", ())
|
||||
|
||||
describe "Parsing wasp element name and properties" $ do
|
||||
|
Loading…
Reference in New Issue
Block a user