ihp-ide-1.5.0: IHP/IDE/CodeGen/ViewGenerator.hs
module IHP.IDE.CodeGen.ViewGenerator (buildPlan, buildPlan', ViewConfig (..), postgresTypeToFieldHelper) where
import IHP.Prelude
import IHP.IDE.CodeGen.Types
import IHP.Postgres.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 <- loadAppSchema
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 =
qualifiedModuleName 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, nameWithoutSuffix) = ensureSuffix "View" name
indexAction = pluralName <> "Action"
specialCases = [
("IndexView", indexView)
, ("ShowView", showView)
, ("EditView", editView)
, ("NewView", newView)
]
paginationEnabled = config.paginationEnabled
tableFound :: Bool
tableFound = [ modelNameToTableName pluralVariableName, pluralVariableName ]
|> mapMaybe (columnsForTable schema)
|> headMay
|> isJust
modelColumns :: [Column]
modelColumns = [ modelNameToTableName pluralVariableName, pluralVariableName ]
|> mapMaybe (columnsForTable schema)
|> head
|> fromMaybe []
foreignKeySet :: [(Text, Text)]
foreignKeySet = [ modelNameToTableName pluralVariableName, pluralVariableName ]
|> map (foreignKeysForTable schema)
|> concat
-- 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
showViewBody =
if null modelColumns
then "<p>{" <> singularVariableName <> "}</p>"
else "<dl>" <> mconcat (map showColumn modelColumns) <> "\n</dl>"
where
showColumn column =
let fieldName = columnNameToFieldName column.name
label = columnNameToFieldLabel column.name
in "\n <dt>" <> label <> "</dt><dd>{" <> singularVariableName <> "." <> fieldName <> "}</dd>"
showView = [trimming|
${viewHeader}
data ShowView = ShowView { ${singularVariableName} :: ${singularName} }
instance View ShowView where
html ShowView { .. } = [hsx|
{breadcrumb}
<h1>Show ${singularName}</h1>
${showViewBody}
${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 columnToFormField modelColumns)
columnToFormField column =
let fieldName = columnNameToFieldName column.name
isForeignKey = any (\(colName, _) -> colName == column.name) foreignKeySet
helper = postgresTypeToFieldHelper column.columnType
in if isForeignKey
then "{- " <> fieldName <> " needs to be a selectField -}\n {(" <> helper <> " #" <> fieldName <> ")}"
else "{(" <> helper <> " #" <> fieldName <> ")}"
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}
|]
indexHeaders =
if null modelColumns
then "<th>" <> singularName <> "</th>"
else intercalate "\n" (map (\c -> "<th>" <> columnNameToFieldLabel c.name <> "</th>") modelColumns)
indexCells =
if null modelColumns
then "<td>{" <> singularVariableName <> "}</td>"
else intercalate "\n" (map (\c -> "<td>{" <> singularVariableName <> "." <> columnNameToFieldName c.name <> "}</td>") modelColumns)
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>
${indexHeaders}
<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>
${indexCells}
<td><a href={${showLink}}>Show</a></td>
<td><a href={${editLink}} class="text-muted">Edit</a></td>
<td><a href={${deleteLink}} 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 ""
idSuffix = if tableFound then " " <> singularVariableName <> ".id" else ""
showLink = "Show" <> singularName <> "Action" <> idSuffix
editLink = "Edit" <> singularName <> "Action" <> idSuffix
deleteLink = "Delete" <> singularName <> "Action" <> idSuffix
chosenView = fromMaybe genericView (lookup nameWithSuffix specialCases)
in
[ EnsureDirectory { directory = textToOsPath (config.applicationName <> "/View/" <> controllerName) }
, CreateFile { filePath = textToOsPath (config.applicationName <> "/View/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs"), fileContent = chosenView }
, AddImport { filePath = textToOsPath (config.applicationName <> "/Controller/" <> controllerName <> ".hs"), fileContent = "import " <> qualifiedViewModuleName config nameWithoutSuffix }
]
-- | Maps a Postgres column type to the appropriate IHP form field helper name.
postgresTypeToFieldHelper :: PostgresType -> Text
postgresTypeToFieldHelper PBoolean = "checkboxField"
postgresTypeToFieldHelper PInt = "numberField"
postgresTypeToFieldHelper PSmallInt = "numberField"
postgresTypeToFieldHelper PBigInt = "numberField"
postgresTypeToFieldHelper PSerial = "numberField"
postgresTypeToFieldHelper PBigserial = "numberField"
postgresTypeToFieldHelper PReal = "numberField"
postgresTypeToFieldHelper PDouble = "numberField"
postgresTypeToFieldHelper (PNumeric _ _) = "numberField"
postgresTypeToFieldHelper PDate = "dateField"
postgresTypeToFieldHelper PTimestamp = "dateTimeField"
postgresTypeToFieldHelper PTimestampWithTimezone = "dateTimeField"
postgresTypeToFieldHelper PTime = "timeField"
postgresTypeToFieldHelper _ = "textField"