ihp-ide-1.4.0: IHP/IDE/CodeGen/ViewGenerator.hs
module IHP.IDE.CodeGen.ViewGenerator (buildPlan, buildPlan', ViewConfig (..)) where
import IHP.Prelude
import qualified Data.Text as Text
import IHP.IDE.CodeGen.Types
import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
import IHP.IDE.SchemaDesigner.Types
import Text.Countable (singularize, pluralize)
data ViewConfig = ViewConfig
{ controllerName :: Text
, applicationName :: Text
, modelName :: Text
, viewName :: Text
, paginationEnabled :: Bool
} deriving (Eq, Show)
buildPlan :: Text -> Text -> Text -> IO (Either Text [GeneratorAction])
buildPlan viewName' applicationName controllerName' =
if (null viewName' || null controllerName')
then pure $ Left "Neither view name nor controller name can be empty"
else do
schema <- SchemaDesigner.parseSchemaSql >>= \case
Left parserError -> pure []
Right statements -> pure statements
let modelName = tableNameToModelName controllerName'
let controllerName = tableNameToControllerName controllerName'
let viewName = tableNameToViewName viewName'
let paginationEnabled = False
let viewConfig = ViewConfig { .. }
pure $ Right $ buildPlan' schema viewConfig
-- E.g. qualifiedViewModuleName config "Edit" == "Web.View.Users.Edit"
qualifiedViewModuleName :: ViewConfig -> Text -> Text
qualifiedViewModuleName config viewName =
config.applicationName <> ".View." <> config.controllerName <> "." <> viewName
buildPlan' :: [Statement] -> ViewConfig -> [GeneratorAction]
buildPlan' schema config =
let
controllerName = config.controllerName
name = config.viewName
singularName = config.modelName |> lcfirst |> singularize |> ucfirst -- TODO: `singularize` Should Support Lower-Cased Words
pluralName = singularName |> lcfirst |> pluralize |> ucfirst -- TODO: `pluralize` Should Support Lower-Cased Words
singularVariableName = lcfirst singularName
pluralVariableName = lcfirst controllerName
nameWithSuffix = if "View" `isSuffixOf` name
then name
else name <> "View" --e.g. "Test" -> "TestView"
nameWithoutSuffix = if "View" `isSuffixOf` name
then Text.replace "View" "" name
else name --e.g. "TestView" -> "Test"
indexAction = pluralName <> "Action"
specialCases = [
("IndexView", indexView)
, ("ShowView", showView)
, ("EditView", editView)
, ("NewView", newView)
]
paginationEnabled = config.paginationEnabled
modelFields :: [Text]
modelFields = [ modelNameToTableName pluralVariableName, pluralVariableName ]
|> mapMaybe (fieldsForTable schema)
|> head
|> fromMaybe []
-- when using the trimming quasiquoter we can't have another |] closure, like for the one we use with hsx.
qqClose = "|]"
viewHeader = [trimming|
module ${moduleName} where
import ${applicationName}.View.Prelude
|]
where
moduleName = qualifiedViewModuleName config nameWithoutSuffix
applicationName = config.applicationName
genericView = [trimming|
${viewHeader}
data ${nameWithSuffix} = ${nameWithSuffix}
instance View ${nameWithSuffix} where
html ${nameWithSuffix} { .. } = [hsx|
{breadcrumb}
<h1>${nameWithSuffix}</h1>
${qqClose}
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "${pluralizedName}" ${indexAction}
, breadcrumbText "${nameWithSuffix}"
]
|]
where
pluralizedName = pluralize name
showView = [trimming|
${viewHeader}
data ShowView = ShowView { ${singularVariableName} :: ${singularName} }
instance View ShowView where
html ShowView { .. } = [hsx|
{breadcrumb}
<h1>Show ${singularName}</h1>
<p>{${singularVariableName}}</p>
${qqClose}
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "${pluralName}" ${indexAction}
, breadcrumbText "Show ${singularName}"
]
|]
-- The form that will appear in New and Edit pages.
renderForm = [trimming|
renderForm :: ${singularName} -> Html
renderForm ${singularVariableName} = formFor ${singularVariableName} [hsx|
${formFields}
{submitButton}
${qqClose}
|]
where
formFields =
intercalate "\n" (map (\field -> "{(textField #" <> field <> ")}") modelFields)
newView = [trimming|
${viewHeader}
data NewView = NewView { ${singularVariableName} :: ${singularName} }
instance View NewView where
html NewView { .. } = [hsx|
{breadcrumb}
<h1>New ${singularName}</h1>
{renderForm ${singularVariableName}}
${qqClose}
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "${pluralName}" ${indexAction}
, breadcrumbText "New ${singularName}"
]
${renderForm}
|]
editView = [trimming|
${viewHeader}
data EditView = EditView { ${singularVariableName} :: ${singularName} }
instance View EditView where
html EditView { .. } = [hsx|
{breadcrumb}
<h1>Edit ${singularName}</h1>
{renderForm ${singularVariableName}}
${qqClose}
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "${pluralName}" ${indexAction}
, breadcrumbText "Edit ${singularName}"
]
${renderForm}
|]
indexView = [trimming|
${viewHeader}
data IndexView = IndexView { ${pluralVariableName} :: [${singularName}]${importPagination} }
instance View IndexView where
html IndexView { .. } = [hsx|
{breadcrumb}
<h1>${nameWithoutSuffix}<a href={pathTo New${singularName}Action} class="btn btn-primary ms-4">+ New</a></h1>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>${singularName}</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>{forEach ${pluralVariableName} render${singularName}}</tbody>
</table>
${renderPagination}
</div>
${qqClose}
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "${pluralName}" ${indexAction}
]
render${singularName} :: ${singularName} -> Html
render${singularName} ${singularVariableName} = [hsx|
<tr>
<td>{${singularVariableName}}</td>
<td><a href={Show${singularName}Action ${singularVariableName}.id}>Show</a></td>
<td><a href={Edit${singularName}Action ${singularVariableName}.id} class="text-muted">Edit</a></td>
<td><a href={Delete${singularName}Action ${singularVariableName}.id} class="js-delete text-muted">Delete</a></td>
</tr>
${qqClose}
|]
where
importPagination = if paginationEnabled then ", pagination :: Pagination" else ""
renderPagination = if paginationEnabled then "{renderPagination pagination}" else ""
chosenView = fromMaybe genericView (lookup nameWithSuffix specialCases)
in
[ EnsureDirectory { directory = config.applicationName <> "/View/" <> controllerName }
, CreateFile { filePath = config.applicationName <> "/View/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs", fileContent = chosenView }
, AddImport { filePath = config.applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = "import " <> qualifiedViewModuleName config nameWithoutSuffix }
]