Implemented parsing and generation of entity-list field render function. (#84)

This commit is contained in:
Matija Sosic 2020-02-03 16:16:45 +01:00 committed by GitHub
parent c695f30704
commit f955fa6a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 284 additions and 75 deletions

View File

@ -48,6 +48,14 @@ export class {= listName =} extends React.Component {
if ({= entityLowerName =}.id === this.state.{= entityBeingEditedStateVar =})
this.setState({ {= entityBeingEditedStateVar =}: null })
}
{=! Render "render" functions for each field, if provided =}
{=# listFields =}
{=# render =}
{= renderFnName =} =
{=& render =}
{=/ render =}
{=/ listFields =}
render() {
const {= entityLowerName =}ListToShow = this.props.filter ?
@ -104,7 +112,12 @@ export class {= listName =} extends React.Component {
)}
/>
) : (
{=# render =}
this.{= renderFnName =}({= entityLowerName =})
{=/ render =}
{=^ render =}
{= entityLowerName =}.{= name =}
{=/ render =}
)}
</TableCell>
</ClickAwayListener>

View File

@ -68,5 +68,18 @@ entity-form<Task> NewTaskForm {
// Entity list definition.
entity-list<Task> TaskList {
// Options TBD, not supported for now.
fields: {
description: {
// The contract for render is: user must provide a function that:
// - receives a task as an input
// - returns a React Node or something that can be rendered by JSX
// - does not depend on any outer context
render: {=js
(task) => {
if (task.isDone) return (<s>{task.description}</s>)
return task.description
}
js=}
}
}
}

View File

@ -105,10 +105,12 @@ entity-list<Task> TaskList {
// - receives a task as an input
// - returns a React Node or something that can be rendered by JSX
// - does not depend on any outer context
render: (task) => {
if (task.isDone) return (<s>task.description</s>)
return task.description
}
render: {=js
(task) => {
if (task.isDone) return (<s>{task.description}</s>)
return task.description
}
js=}
}
}
}

View File

@ -9,14 +9,18 @@ import Data.Maybe (fromJust)
import Path ((</>), reldir, relfile, parseRelFile)
import qualified Path.Aliases as Path
import qualified Util as U
import qualified Wasp
import Wasp (Wasp)
import qualified Wasp.EntityList as WEL
import qualified Wasp.JsCode
import qualified Generator.FileDraft as FD
import qualified Generator.Entity.Common as EC
import qualified Generator.Common as Common
data EntityListTemplateData = EntityListTemplateData
{ _listName :: !String
, _entityName :: !String
@ -40,6 +44,8 @@ instance ToJSON EntityListTemplateData where
data ListFieldTemplateData = ListFieldTemplateData
{ _fieldName :: !String
, _fieldType :: !Wasp.EntityFieldType
, _fieldRender :: Maybe Wasp.JsCode.JsCode
, _fieldRenderFnName :: String
}
instance ToJSON ListFieldTemplateData where
@ -47,6 +53,8 @@ instance ToJSON ListFieldTemplateData where
object
[ "name" .= _fieldName f
, "type" .= _fieldType f
, "render" .= _fieldRender f
, "renderFnName" .= _fieldRenderFnName f
]
createEntityListTemplateData :: Wasp.Entity -> WEL.EntityList -> EntityListTemplateData
@ -58,17 +66,26 @@ createEntityListTemplateData entity entityList =
, _entityName = Wasp.entityName entity
, _entityClassName = EC.getEntityClassName entity
, _entityLowerName = EC.getEntityLowerName entity
, _listFields = map (createListFieldTD entityList) $ Wasp.entityFields entity
, _listFields = map (createListFieldTD entity entityList) $ Wasp.entityFields entity
, _entityBeingEditedStateVar = entityLowerName ++ "BeingEdited"
}
where
entityLowerName = EC.getEntityLowerName entity
createListFieldTD :: WEL.EntityList -> Wasp.EntityField -> ListFieldTemplateData
createListFieldTD _ entityField = ListFieldTemplateData
createListFieldTD :: Wasp.Entity -> WEL.EntityList -> Wasp.EntityField -> ListFieldTemplateData
createListFieldTD entity entityList entityField = ListFieldTemplateData
{ _fieldName = Wasp.entityFieldName entityField
, _fieldType = Wasp.entityFieldType entityField
, _fieldRender = listFieldConfig >>= WEL._fieldRender
, _fieldRenderFnName = "render" ++ entityUpper ++ entityFieldUpper
}
where
-- Configuration of a form field within entity-list, if there is any.
listFieldConfig :: Maybe WEL.Field
listFieldConfig = WEL.getConfigForField entityList entityField
entityUpper = U.toUpperFirst $ Wasp.entityName entity
entityFieldUpper = U.toUpperFirst $ Wasp.entityFieldName entityField
generateEntityList :: Wasp -> WEL.EntityList -> FD.FileDraft
generateEntityList wasp entityList =

View File

@ -12,8 +12,8 @@ import Lexer
import Parser.App (app)
import Parser.Page (page)
import Parser.Entity (entity)
import Parser.EntityForm (entityForm)
import Parser.EntityList (entityList)
import Parser.Entity.EntityForm (entityForm)
import Parser.Entity.EntityList (entityList)
import Parser.JsImport (jsImport)
import Parser.Common (runWaspParser)

View File

@ -12,6 +12,7 @@ import qualified Path.Aliases as Path
import qualified Lexer as L
-- | Runs given wasp parser on a specified input.
runWaspParser :: Parser a -> String -> Either ParseError a
runWaspParser waspParser input = parse waspParser sourceName input

View File

@ -0,0 +1,39 @@
module Parser.Entity.Common
( waspPropertyEntityFields
) where
import Text.Parsec.String (Parser)
import qualified Lexer as L
import Parser.Common as P
-- A function that takes an entity field name (e.g. "description) and a list of parsed field
-- options, and then creates a final Wasp AST record from it (fieldConfig).
type CreateFieldConfig fieldOption fieldConfig = (String, [fieldOption]) -> fieldConfig
-- | Parses configuration of fields within a wasp entity component (e.g. entity-form
-- or entity-list). Parses the following format:
--
-- fields: { FIELD_NAME: {...}, FIELD_NAME: {...}, ... }
--
-- At least one field must be specified.
waspPropertyEntityFields
:: Parser fo -- ^ Parser of a single field option.
-> CreateFieldConfig fo fc -- ^ Function that creates a record with all parsed field options.
-> Parser [fc] -- ^ Field configs, a list of record with all the field options.
waspPropertyEntityFields fieldOptionP createFieldConfig = P.waspPropertyClosure "fields" $
L.commaSep1 $ waspPropertyEntityField fieldOptionP createFieldConfig
-- | Parses configuration of a specific field within a wasp entity component (e.g. entity-form
-- or entity-list). Parses the following format:
--
-- FIELD_NAME: { option1, option2 }
--
-- At least one option must be present.
waspPropertyEntityField
:: Parser fo -- ^ Parser of a single field option.
-> CreateFieldConfig fo fc -- ^ Function that creates a record with all parsed field options.
-> Parser fc -- ^ Field config, a record with all the field options.
waspPropertyEntityField fieldOptionP createFieldConfig =
(P.waspIdentifierClosure $ L.commaSep1 fieldOptionP) >>= (return . createFieldConfig)

View File

@ -1,4 +1,4 @@
module Parser.EntityForm
module Parser.Entity.EntityForm
( entityForm
-- For testing
@ -11,10 +11,11 @@ module Parser.EntityForm
import Text.Parsec (choice)
import Text.Parsec.String (Parser)
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityForm as WEF
import Wasp.EntityForm (EntityForm)
import qualified Parser.Common as P
import qualified Parser.Entity.Common as PE
import qualified Util as U
import qualified Lexer as L
@ -27,16 +28,16 @@ entityForm = do
(entityName, formName, options) <-
P.waspElementLinkedToEntity L.reservedNameEntityForm entityFormOptions
return EF.EntityForm
{ EF._name = formName
, EF._entityName = entityName
, EF._submit = maybeGetSubmitConfig options
, EF._fields = getFieldsConfig options
return WEF.EntityForm
{ WEF._name = formName
, WEF._entityName = entityName
, WEF._submit = maybeGetSubmitConfig options
, WEF._fields = getFieldsConfig options
}
data EntityFormOption
= EfoSubmit EF.Submit
| EfoFields [EF.Field]
= EfoSubmit WEF.Submit
| EfoFields [WEF.Field]
deriving (Show, Eq)
entityFormOptions :: Parser [EntityFormOption]
@ -50,25 +51,25 @@ entityFormOption = choice
-- * Submit
maybeGetSubmitConfig :: [EntityFormOption] -> Maybe EF.Submit
maybeGetSubmitConfig :: [EntityFormOption] -> Maybe WEF.Submit
maybeGetSubmitConfig options = U.headSafe [s | EfoSubmit s <- options]
entityFormOptionSubmit :: Parser EntityFormOption
entityFormOptionSubmit = EfoSubmit <$> (P.waspPropertyClosure "submit" submitConfig)
submitConfig :: Parser EF.Submit
submitConfig :: Parser WEF.Submit
submitConfig = do
-- TODO(matija): this pattern of "having at least 1 property in closure" could be further
-- extracted to e.g. "waspClosureOptions" - but again sometimes it is ok not to have any props,
-- e.g. EntityForm. Maybe then "waspClosureOptions1" and "waspClosureOptions"?
options <- L.commaSep1 submitOption
return EF.Submit
{ EF._onEnter = maybeGetSoOnEnter options
, EF._submitButton = maybeGetSoSubmitButton options
return WEF.Submit
{ WEF._onEnter = maybeGetSoOnEnter options
, WEF._submitButton = maybeGetSoSubmitButton options
}
data SubmitOption = SoOnEnter Bool | SoSubmitButton EF.SubmitButton deriving (Show, Eq)
data SubmitOption = SoOnEnter Bool | SoSubmitButton WEF.SubmitButton deriving (Show, Eq)
submitOption :: Parser SubmitOption
submitOption = choice [submitOptionOnEnter, submitOptionSubmitButton]
@ -84,15 +85,15 @@ maybeGetSoOnEnter options = U.headSafe [b | SoOnEnter b <- options]
submitOptionSubmitButton :: Parser SubmitOption
submitOptionSubmitButton = SoSubmitButton <$> P.waspPropertyClosure "button" submitButtonConfig
maybeGetSoSubmitButton :: [SubmitOption] -> Maybe EF.SubmitButton
maybeGetSoSubmitButton :: [SubmitOption] -> Maybe WEF.SubmitButton
maybeGetSoSubmitButton options = U.headSafe [sb | SoSubmitButton sb <- options]
submitButtonConfig :: Parser EF.SubmitButton
submitButtonConfig :: Parser WEF.SubmitButton
submitButtonConfig = do
options <- L.commaSep1 submitButtonOption
return EF.SubmitButton
{ EF._submitButtonShow = maybeGetSboShow options
return WEF.SubmitButton
{ WEF._submitButtonShow = maybeGetSboShow options
}
data SubmitButtonOption = SboShow Bool deriving (Show, Eq)
@ -108,28 +109,24 @@ maybeGetSboShow options = U.headSafe [b | SboShow b <- options]
-- * Fields
getFieldsConfig :: [EntityFormOption] -> [EF.Field]
getFieldsConfig :: [EntityFormOption] -> [WEF.Field]
getFieldsConfig options = case [fs | EfoFields fs <- options] of
[] -> []
ls -> head ls
entityFormOptionFields :: Parser EntityFormOption
entityFormOptionFields = EfoFields <$> (P.waspPropertyClosure "fields" $ L.commaSep1 field)
entityFormOptionFields = EfoFields <$> PE.waspPropertyEntityFields fieldOption createFieldConfig
-- | Parses 'FIELD_NAME: { ... }.'
field :: Parser EF.Field
field = do
(fieldName, options) <- P.waspIdentifierClosure $ L.commaSep1 fieldOption
return EF.Field
{ EF._fieldName = fieldName
, EF._fieldShow = maybeGetFieldOptionShow options
, EF._fieldDefaultValue = maybeGetFieldOptionDefaultValue options
}
createFieldConfig :: (String, [FieldOption]) -> WEF.Field
createFieldConfig (fieldName, options) = WEF.Field
{ WEF._fieldName = fieldName
, WEF._fieldShow = maybeGetFieldOptionShow options
, WEF._fieldDefaultValue = maybeGetFieldOptionDefaultValue options
}
data FieldOption
= FieldOptionShow Bool
| FieldOptionDefaultValue EF.DefaultValue
| FieldOptionDefaultValue WEF.DefaultValue
deriving (Show, Eq)
-- | Parses a single field option, e.g. "show" or "defaultValue".
@ -139,14 +136,14 @@ fieldOption = choice
, FieldOptionDefaultValue <$> defaultValue
]
defaultValue :: Parser EF.DefaultValue
defaultValue :: Parser WEF.DefaultValue
defaultValue = P.waspProperty "defaultValue" $ choice
[ EF.DefaultValueString <$> L.stringLiteral
, EF.DefaultValueBool <$> L.bool
[ WEF.DefaultValueString <$> L.stringLiteral
, WEF.DefaultValueBool <$> L.bool
]
maybeGetFieldOptionShow :: [FieldOption] -> Maybe Bool
maybeGetFieldOptionShow options = U.headSafe [b | FieldOptionShow b <- options]
maybeGetFieldOptionDefaultValue :: [FieldOption] -> Maybe EF.DefaultValue
maybeGetFieldOptionDefaultValue :: [FieldOption] -> Maybe WEF.DefaultValue
maybeGetFieldOptionDefaultValue options = U.headSafe [dv | FieldOptionDefaultValue dv <- options]

View File

@ -0,0 +1,75 @@
module Parser.Entity.EntityList
( entityList
) where
import Text.Parsec (choice)
import Text.Parsec.String (Parser)
import qualified Wasp.EntityList as WEL
import Wasp.EntityList (EntityList)
import qualified Wasp.JsCode as WJS
import qualified Parser.JsCode
import qualified Parser.Common as P
import qualified Parser.Entity.Common as PE
import qualified Util as U
import qualified Lexer as L
-- * EntityList
-- | Parses entity list, e.g. "entity-list<Task> TaskList {...}"
entityList :: Parser EntityList
entityList = do
(entityName, listName, options) <-
P.waspElementLinkedToEntity L.reservedNameEntityList entityListOptions
return WEL.EntityList
{ WEL._name = listName
, WEL._entityName = entityName
, WEL._fields = getFieldsConfig options
}
data EntityListOption
= EloFields [WEL.Field]
deriving (Show, Eq)
entityListOptions :: Parser [EntityListOption]
-- TODO(matija): this could be further abstracted as waspClosureOptions option ->
-- that way we abstract L.commaSep
entityListOptions = L.commaSep entityListOption
entityListOption :: Parser EntityListOption
entityListOption = choice
[ entityListOptionFields
]
-- * Fields
getFieldsConfig :: [EntityListOption] -> [WEL.Field]
getFieldsConfig options = case [fs | EloFields fs <- options] of
[] -> []
ls -> head ls
entityListOptionFields :: Parser EntityListOption
entityListOptionFields = EloFields <$> PE.waspPropertyEntityFields fieldOption createFieldConfig
createFieldConfig :: (String, [FieldOption]) -> WEL.Field
createFieldConfig (fieldName, options) = WEL.Field
{ WEL._fieldName = fieldName
, WEL._fieldRender = maybeGetFieldOptionRender options
}
data FieldOption
= FieldOptionRender WJS.JsCode
fieldOption :: Parser FieldOption
fieldOption = choice
[ fieldOptionRender
]
fieldOptionRender :: Parser FieldOption
fieldOptionRender = FieldOptionRender <$> P.waspProperty "render" Parser.JsCode.jsCode
maybeGetFieldOptionRender :: [FieldOption] -> Maybe WJS.JsCode
maybeGetFieldOptionRender options = U.headSafe [js | FieldOptionRender js <- options]

View File

@ -1,26 +0,0 @@
module Parser.EntityList
( entityList
) where
import Text.Parsec.String (Parser)
import Text.Parsec.Char (spaces)
import qualified Wasp.EntityList as EL
import Wasp.EntityList (EntityList)
import qualified Parser.Common as P
import qualified Lexer as L
-- * EntityList
-- | Parses entity list, e.g. "entity-list<Task> TaskList {...}"
entityList :: Parser EntityList
entityList = do
(entityName, listName, _) <-
-- NOTE(matija): not supporting any options yet.
P.waspElementLinkedToEntity L.reservedNameEntityList spaces
return EL.EntityList
{ EL._name = listName
, EL._entityName = entityName
}

12
src/Parser/JsCode.hs Normal file
View File

@ -0,0 +1,12 @@
module Parser.JsCode
( jsCode
) where
import Text.Parsec.String (Parser)
import qualified Data.Text as Text
import qualified Parser.Common as P
import qualified Wasp.JsCode as WJS
jsCode :: Parser WJS.JsCode
jsCode = (WJS.JsCode . Text.pack) <$> P.waspNamedClosure "js"

View File

@ -21,6 +21,12 @@ data EntityForm = EntityForm
, _fields :: [Field]
} deriving (Show, Eq)
-- NOTE(matija): Ideally generator would not depend on this logic defined outside of it.
-- We are moving away from this approach but some parts of code (Page generator) still
-- rely on it so we cannot remove it completely yet without further refactoring.
--
-- Some record fields are note even included (e.g. _fields), we are keeping this only for the
-- backwards compatibility.
instance ToJSON EntityForm where
toJSON entityForm = object
[ "name" .= _name entityForm

View File

@ -1,16 +1,49 @@
module Wasp.EntityList
( EntityList(..)
, Field(..)
, getConfigForField
) where
import Data.Aeson ((.=), object, ToJSON(..))
import Wasp.JsCode (JsCode)
import qualified Util as U
import qualified Wasp.Entity as Entity
data EntityList = EntityList
{ _name :: !String -- Name of the list
, _entityName :: !String -- Name of the entity the form is linked to
, _fields :: [Field]
} deriving (Show, Eq)
-- NOTE(matija): Ideally generator would not depend on this logic defined outside of it.
-- We are moving away from this approach but some parts of code (Page generator) still
-- rely on it so we cannot remove it completely yet without further refactoring.
--
-- Some record fields are note even included (e.g. _fields), we are keeping this only for the
-- backwards compatibility.
instance ToJSON EntityList where
toJSON entityList = object
[ "name" .= _name entityList
, "entityName" .= _entityName entityList
]
-- | For a given entity field, returns its configuration from the given entity-list, if present.
-- TODO(matija): this is very similar to the same function in EntityForm, we could extract it
-- (prob. using typeclass or TH) in the future.
getConfigForField :: EntityList -> Entity.EntityField -> Maybe Field
getConfigForField entityList entityField =
U.headSafe $ filter isConfigOfInputEntityField $ _fields entityList
where
isConfigOfInputEntityField :: Field -> Bool
isConfigOfInputEntityField =
(== Entity.entityFieldName entityField) . _fieldName
-- * Field
data Field = Field
{ _fieldName :: !String
, _fieldRender :: Maybe JsCode -- Js function that renders a list field.
} deriving (Show, Eq)

14
src/Wasp/JsCode.hs Normal file
View File

@ -0,0 +1,14 @@
module Wasp.JsCode
( JsCode(..)
) where
import Data.Aeson (ToJSON(..))
import Data.Text (Text)
data JsCode = JsCode !Text deriving (Show, Eq)
-- TODO(matija): Currently generator is relying on this implementation, which is not
-- ideal. Ideally all the generation logic would be in the generator. But for now this was
-- the simplest way to implement it.
instance ToJSON JsCode where
toJSON (JsCode code) = toJSON code

View File

@ -3,7 +3,7 @@ module Parser.EntityFormTest where
import Test.Tasty.Hspec
import Parser.Common (runWaspParser)
import Parser.EntityForm
import Parser.Entity.EntityForm
( entityForm
, submitConfig

View File

@ -9,6 +9,7 @@ import Wasp
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityList as EL
import qualified Wasp.Style
import qualified Wasp.JsCode
spec_parseWasp :: Spec
@ -88,6 +89,12 @@ spec_parseWasp =
, WaspElementEntityList $ EL.EntityList
{ EL._name = "TaskList"
, EL._entityName = "Task"
, EL._fields =
[ EL.Field
{ EL._fieldName = "description"
, EL._fieldRender = Just $ Wasp.JsCode.JsCode "task => task.description"
}
]
}
]
`setJsImports` [ JsImport "something" [relfile|some/file|] ]

View File

@ -69,5 +69,11 @@ entity-form<Task> CreateTaskForm {
}
entity-list<Task> TaskList {
// Options TBD.
fields: {
description: {
render: {=js
task => task.description
js=}
}
}
}