diff --git a/data/Generator/templates/react-app/src/entities/_entity/components/List.js b/data/Generator/templates/react-app/src/entities/_entity/components/List.js index 2ca1fa503..2fa9fbb24 100644 --- a/data/Generator/templates/react-app/src/entities/_entity/components/List.js +++ b/data/Generator/templates/react-app/src/entities/_entity/components/List.js @@ -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 =} )} diff --git a/examples/todoApp/todoApp.wasp b/examples/todoApp/todoApp.wasp index 908dcf1be..75f068736 100644 --- a/examples/todoApp/todoApp.wasp +++ b/examples/todoApp/todoApp.wasp @@ -68,5 +68,18 @@ entity-form NewTaskForm { // Entity list definition. entity-list 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 ({task.description}) + return task.description + } + js=} + } + } } diff --git a/lang-design/todoApp.wasp b/lang-design/todoApp.wasp index 5d1e93462..9d89287e2 100644 --- a/lang-design/todoApp.wasp +++ b/lang-design/todoApp.wasp @@ -105,10 +105,12 @@ entity-list 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 (task.description) - return task.description - } + render: {=js + (task) => { + if (task.isDone) return ({task.description}) + return task.description + } + js=} } } } diff --git a/src/Generator/Entity/EntityList.hs b/src/Generator/Entity/EntityList.hs index 87f6ce7c5..ae1bf22b3 100644 --- a/src/Generator/Entity/EntityList.hs +++ b/src/Generator/Entity/EntityList.hs @@ -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 = diff --git a/src/Parser.hs b/src/Parser.hs index c50581dd5..8f4e1785d 100644 --- a/src/Parser.hs +++ b/src/Parser.hs @@ -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) diff --git a/src/Parser/Common.hs b/src/Parser/Common.hs index 95ddb0a0f..19786b537 100644 --- a/src/Parser/Common.hs +++ b/src/Parser/Common.hs @@ -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 diff --git a/src/Parser/Entity/Common.hs b/src/Parser/Entity/Common.hs new file mode 100644 index 000000000..4dbf01247 --- /dev/null +++ b/src/Parser/Entity/Common.hs @@ -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) diff --git a/src/Parser/EntityForm.hs b/src/Parser/Entity/EntityForm.hs similarity index 67% rename from src/Parser/EntityForm.hs rename to src/Parser/Entity/EntityForm.hs index 3777f6a8a..429eeaacd 100644 --- a/src/Parser/EntityForm.hs +++ b/src/Parser/Entity/EntityForm.hs @@ -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] diff --git a/src/Parser/Entity/EntityList.hs b/src/Parser/Entity/EntityList.hs new file mode 100644 index 000000000..d92f988df --- /dev/null +++ b/src/Parser/Entity/EntityList.hs @@ -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 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] diff --git a/src/Parser/EntityList.hs b/src/Parser/EntityList.hs deleted file mode 100644 index e33291740..000000000 --- a/src/Parser/EntityList.hs +++ /dev/null @@ -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 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 - } diff --git a/src/Parser/JsCode.hs b/src/Parser/JsCode.hs new file mode 100644 index 000000000..65713ca13 --- /dev/null +++ b/src/Parser/JsCode.hs @@ -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" diff --git a/src/Wasp/EntityForm.hs b/src/Wasp/EntityForm.hs index 83e0a362d..3567a2d40 100644 --- a/src/Wasp/EntityForm.hs +++ b/src/Wasp/EntityForm.hs @@ -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 diff --git a/src/Wasp/EntityList.hs b/src/Wasp/EntityList.hs index e17f7cb94..f6cf4f4a4 100644 --- a/src/Wasp/EntityList.hs +++ b/src/Wasp/EntityList.hs @@ -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) diff --git a/src/Wasp/JsCode.hs b/src/Wasp/JsCode.hs new file mode 100644 index 000000000..f9a811327 --- /dev/null +++ b/src/Wasp/JsCode.hs @@ -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 diff --git a/test/Parser/EntityFormTest.hs b/test/Parser/EntityFormTest.hs index 07119d9e5..9beda87d0 100644 --- a/test/Parser/EntityFormTest.hs +++ b/test/Parser/EntityFormTest.hs @@ -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 diff --git a/test/Parser/ParserTest.hs b/test/Parser/ParserTest.hs index f91273d81..99eedd382 100644 --- a/test/Parser/ParserTest.hs +++ b/test/Parser/ParserTest.hs @@ -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|] ] diff --git a/test/Parser/valid.wasp b/test/Parser/valid.wasp index 99d722b2b..de660c9dd 100644 --- a/test/Parser/valid.wasp +++ b/test/Parser/valid.wasp @@ -69,5 +69,11 @@ entity-form CreateTaskForm { } entity-list TaskList { - // Options TBD. + fields: { + description: { + render: {=js + task => task.description + js=} + } + } }