diff --git a/waspc/data/Generator/templates/react-app/src/components/_Button.js b/waspc/data/Generator/templates/react-app/src/components/_Button.js new file mode 100644 index 000000000..d28430e19 --- /dev/null +++ b/waspc/data/Generator/templates/react-app/src/components/_Button.js @@ -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 ( + + ) + } +} + +export default connect(state => ({ + // Selectors +}), { + // Actions + {=# onClickAction =} + {= exportedIdentifier =} + {=/ onClickAction =} +})({= button.name =}) diff --git a/waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js b/waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js index ee06dd3d9..852dce035 100644 --- a/waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js +++ b/waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js @@ -1,7 +1,7 @@ {{={= =}=}} import uuidv4 from 'uuid/v4' -export default class {= entity.name =} { +export default class {= entityClassName =} { _data = {} constructor (data = {}) { diff --git a/waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js b/waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js index 0a0ed0afb..917ac07f4 100644 --- a/waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js +++ b/waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js @@ -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' diff --git a/waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js b/waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js index 3e53095e5..5e0d7b7d7 100644 --- a/waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js +++ b/waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js @@ -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 =} diff --git a/waspc/data/Generator/templates/react-app/src/entities/_entity/state.js b/waspc/data/Generator/templates/react-app/src/entities/_entity/state.js index fee090cf8..ccab55547 100644 --- a/waspc/data/Generator/templates/react-app/src/entities/_entity/state.js +++ b/waspc/data/Generator/templates/react-app/src/entities/_entity/state.js @@ -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)) }) diff --git a/waspc/examples/todoApp/ext/Main.css b/waspc/examples/todoApp/ext/Main.css index 7ce3eeef3..74c5275e6 100644 --- a/waspc/examples/todoApp/ext/Main.css +++ b/waspc/examples/todoApp/ext/Main.css @@ -38,7 +38,7 @@ button.selected { background-color: white; border: none; display: inline-block; - font-size: 24px; + font-size: 24px !important; cursor: pointer; } diff --git a/waspc/examples/todoApp/ext/Todo.js b/waspc/examples/todoApp/ext/Todo.js index 36da7263b..1529d0d54 100644 --- a/waspc/examples/todoApp/ext/Todo.js +++ b/waspc/examples/todoApp/ext/Todo.js @@ -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 {

Todos

- + /> { this.isThereAnyTask() && (<> - +
@@ -61,11 +49,9 @@ class Todo extends React.Component {
- +
)} @@ -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) diff --git a/waspc/examples/todoApp/todoApp.wasp b/waspc/examples/todoApp/todoApp.wasp index 874d964be..551f836cb 100644 --- a/waspc/examples/todoApp/todoApp.wasp +++ b/waspc/examples/todoApp/todoApp.wasp @@ -52,3 +52,24 @@ entity-list TaskList { active: {=js task => !task.isDone js=} } } + +button ToggleIsDoneButton { + label: "✓", + onClick: toggleIsDoneAction +} + +button DeleteDoneButton { + label: "Delete completed", + onClick: deleteDoneAction +} + +action toggleIsDoneAction {=js + tasks => { + const areAllDone = tasks.every(t => t.isDone) + return tasks.map(t => ({ ...t, isDone: !areAllDone })) + } +js=} + +action deleteDoneAction {=js + tasks => tasks.filter(t => !t.isDone) +js=} diff --git a/waspc/src/Generator/Button.hs b/waspc/src/Generator/Button.hs new file mode 100644 index 000000000..6f27248cd --- /dev/null +++ b/waspc/src/Generator/Button.hs @@ -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) diff --git a/waspc/src/Generator/Entity.hs b/waspc/src/Generator/Entity.hs index f5712ba08..9ad4cb29d 100644 --- a/waspc/src/Generator/Entity.hs +++ b/waspc/src/Generator/Entity.hs @@ -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) diff --git a/waspc/src/Generator/Entity/Common.hs b/waspc/src/Generator/Entity/Common.hs index 966b9e7e1..47f01129d 100644 --- a/waspc/src/Generator/Entity/Common.hs +++ b/waspc/src/Generator/Entity/Common.hs @@ -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. diff --git a/waspc/src/Generator/Generators.hs b/waspc/src/Generator/Generators.hs index 8d95bf906..f9b43a8d9 100644 --- a/waspc/src/Generator/Generators.hs +++ b/waspc/src/Generator/Generators.hs @@ -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 diff --git a/waspc/src/Lexer.hs b/waspc/src/Lexer.hs index 6e43b996f..ea8a1733b 100644 --- a/waspc/src/Lexer.hs +++ b/waspc/src/Lexer.hs @@ -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 diff --git a/waspc/src/Parser.hs b/waspc/src/Parser.hs index 8f4e1785d..b97cd18ee 100644 --- a/waspc/src/Parser.hs +++ b/waspc/src/Parser.hs @@ -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 diff --git a/waspc/src/Parser/Action.hs b/waspc/src/Parser/Action.hs new file mode 100644 index 000000000..0d8bfbcfe --- /dev/null +++ b/waspc/src/Parser/Action.hs @@ -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 + } diff --git a/waspc/src/Parser/Button.hs b/waspc/src/Parser/Button.hs new file mode 100644 index 000000000..4bc84c6a4 --- /dev/null +++ b/waspc/src/Parser/Button.hs @@ -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 + } diff --git a/waspc/src/Parser/Common.hs b/waspc/src/Parser/Common.hs index 6f772e508..7874af138 100644 --- a/waspc/src/Parser/Common.hs +++ b/waspc/src/Parser/Common.hs @@ -50,18 +50,17 @@ waspElementNameAndClosure elementType closure = return (elementName, closureContent) -- | Parses declaration of a wasp element linked to an entity. --- E.g. "entity-form {...}" or "entity-list {...}" +-- E.g. "entity-form ..." or "action ..." 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 diff --git a/waspc/src/Parser/Entity/EntityForm.hs b/waspc/src/Parser/Entity/EntityForm.hs index 644893ea0..03cbbe4b6 100644 --- a/waspc/src/Parser/Entity/EntityForm.hs +++ b/waspc/src/Parser/Entity/EntityForm.hs @@ -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 diff --git a/waspc/src/Parser/Entity/EntityList.hs b/waspc/src/Parser/Entity/EntityList.hs index e844167cb..7c37d70bc 100644 --- a/waspc/src/Parser/Entity/EntityList.hs +++ b/waspc/src/Parser/Entity/EntityList.hs @@ -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 diff --git a/waspc/src/Wasp.hs b/waspc/src/Wasp.hs index 85ba20e02..bc47ee1cd 100644 --- a/waspc/src/Wasp.hs +++ b/waspc/src/Wasp.hs @@ -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] diff --git a/waspc/src/Wasp/Action.hs b/waspc/src/Wasp/Action.hs new file mode 100644 index 000000000..c0cd3aaab --- /dev/null +++ b/waspc/src/Wasp/Action.hs @@ -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 + ] diff --git a/waspc/src/Wasp/Button.hs b/waspc/src/Wasp/Button.hs new file mode 100644 index 000000000..ff09d96d0 --- /dev/null +++ b/waspc/src/Wasp/Button.hs @@ -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 + ] diff --git a/waspc/test/Parser/CommonTest.hs b/waspc/test/Parser/CommonTest.hs index 899b83ab5..a2937e4c6 100644 --- a/waspc/test/Parser/CommonTest.hs +++ b/waspc/test/Parser/CommonTest.hs @@ -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 TaskForm { }" + runWaspParser (waspElementLinkedToEntity "entity-form" (waspClosure whiteSpace)) "entity-form TaskForm { }" `shouldBe` Right ("Task", "TaskForm", ()) describe "Parsing wasp element name and properties" $ do