rewrite forum forms

formRefactor
Thibault Duplessis 2018-11-28 19:32:17 +07:00
parent 68377f6a56
commit 1dfc4a9054
11 changed files with 83 additions and 116 deletions

View File

@ -21,37 +21,37 @@ object ForumPost extends LilaController with ForumController {
}
def create(categSlug: String, slug: String, page: Int) = OpenBody { implicit ctx =>
CreateRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(topicApi.show(categSlug, slug, page, ctx.troll)) {
case (categ, topic, posts) =>
if (topic.closed) fuccess(BadRequest("This topic is closed"))
else if (topic.isOld) fuccess(BadRequest("This topic is archived"))
else forms.post.bindFromRequest.fold(
err => for {
captcha <- forms.anyCaptcha
unsub <- ctx.userId ?? Env.timeline.status(s"forum:${topic.id}")
canModCateg <- isGrantedMod(categ.slug)
} yield BadRequest(html.forum.topic.show(categ, topic, posts, Some(err -> captcha), unsub, canModCateg = canModCateg)),
data => postApi.makePost(categ, topic, data) map { post =>
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(topicApi.show(categSlug, slug, page, ctx.troll)) {
case (categ, topic, posts) =>
if (topic.closed) fuccess(BadRequest("This topic is closed"))
else if (topic.isOld) fuccess(BadRequest("This topic is archived"))
else forms.post.bindFromRequest.fold(
err => for {
captcha <- forms.anyCaptcha
unsub <- ctx.userId ?? Env.timeline.status(s"forum:${topic.id}")
canModCateg <- isGrantedMod(categ.slug)
} yield BadRequest(html.forum.topic.show(categ, topic, posts, Some(err -> captcha), unsub, canModCateg = canModCateg)),
data => CreateRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
postApi.makePost(categ, topic, data) map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
)
}
}
)
}
}
}
def edit(postId: String) = AuthBody { implicit ctx => me =>
implicit val req = ctx.body
forms.postEdit.bindFromRequest.fold(
err => Redirect(routes.ForumPost.redirect(postId)).fuccess,
data =>
data => CreateRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
postApi.editPost(postId, data.changes, me).map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
}
)
}

View File

@ -16,28 +16,26 @@ object ForumTopic extends LilaController with ForumController {
def form(categSlug: String) = Open { implicit ctx =>
NotForKids {
CategGrantWrite(categSlug) {
OptionFuOk(CategRepo bySlug categSlug) { categ =>
forms.anyCaptcha map { html.forum.topic.form(categ, forms.topic, _) }
}
OptionFuOk(CategRepo bySlug categSlug) { categ =>
forms.anyCaptcha map { html.forum.topic.form(categ, forms.topic, _) }
}
}
}
def create(categSlug: String) = OpenBody { implicit ctx =>
CreateRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(CategRepo bySlug categSlug) { categ =>
forms.topic.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.forum.topic.form(categ, err, captcha))
},
data => topicApi.makeTopic(categ, data) map { topic =>
CategGrantWrite(categSlug) {
implicit val req = ctx.body
OptionFuResult(CategRepo bySlug categSlug) { categ =>
forms.topic.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.forum.topic.form(categ, err, captcha))
},
data => CreateRateLimit(HTTPRequest lastRemoteAddress ctx.req) {
topicApi.makeTopic(categ, data.pp) map { topic =>
Redirect(routes.ForumTopic.show(categ.slug, topic.slug, 1))
}
)
}
}
)
}
}
}

View File

@ -33,6 +33,8 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
cssTag(name).body
} mkString ""
}
def cssTags(names: Map[String, Boolean]): Html =
cssTags(names.collect { case (k, true) => k }.toList: _*)
def cssVendorTag(name: String) = cssAt("vendor/" + name)

View File

@ -69,18 +69,29 @@ trait FormHelper { self: I18nHelper =>
s"""<select id="${id(field)}" name="${field.name}" class="form-control">$defaultH$optionsH</select>"""
}
def textarea(field: Field, required: Boolean = false, rows: Option[Int] = None) = Html {
def textarea(
field: Field,
klass: String = "",
required: Boolean = false,
rows: Option[Int] = None,
attrs: String = ""
) = Html {
val rowsH = rows ?? { r => s""" rows=$r""" }
s"""<textarea id="${id(field)}" name="${field.name}" class="form-control"$rowsH${required ?? " required"}>${~field.value}</textarea>"""
s"""<textarea id="${id(field)}" name="${field.name}" class="form-control $klass"$rowsH${required ?? " required"}$attrs>${~field.value}</textarea>"""
}
def actions(html: Html) = Html {
s"""<div class="form-actions">$html</div>"""
}
def submit(name: Html, icon: Option[String] = Some("E")) = Html {
def submit(
content: Html,
icon: Option[String] = Some("E"),
nameValue: Option[(String, String)] = None
) = Html {
val iconH = icon ?? { i => s""" data-icon="$i"""" }
s"""<button class="submit button${icon.isDefined ?? " text"} type="submit"$iconH>$name</button>"""
val nameH = nameValue ?? { case (n, v) => s""" name="$n" value="$v"""" }
s"""<button class="submit button${icon.isDefined ?? " text"} type="submit"$iconH$nameH>$content</button>"""
}
def hidden(field: Field, value: Option[String] = None) = Html {
@ -96,10 +107,11 @@ trait FormHelper { self: I18nHelper =>
minLength: Int = 0,
maxLength: Int = 0,
autocomplete: Boolean = true,
autofocus: Boolean = false,
pattern: Option[String] = None,
attrs: String = ""
) = Html {
val options = s"""${placeholder ?? { p => s""" placeholder="$p"""" }}${required ?? " required"}${(minLength > 0) ?? s"minlength=$minLength"}${(maxLength > 0) ?? s"maxlength=$maxLength"}${!autocomplete ?? """autocomplete="off""""}${pattern ?? { p => s""" pattern="$p"""" }} $attrs"""
val options = s"""${placeholder ?? { p => s""" placeholder="$p"""" }}${required ?? " required"}${(minLength > 0) ?? s"minlength=$minLength"}${(maxLength > 0) ?? s"maxlength=$maxLength"}${!autocomplete ?? """autocomplete="off""""}${autofocus ?? " autofocus"}${pattern ?? { p => s""" pattern="$p"""" }} $attrs"""
s"""<input type="typ" id="${id(field)}" name="${field.name}" value="${~field.value}"$options class="form-control $klass"/>"""
}
}

View File

@ -1,10 +1,10 @@
@(title: String, searchText: String = "", side: Option[Html] = None, menu: Option[Html] = None, moreJs: Html = emptyHtml, openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
@(title: String, searchText: String = "", side: Option[Html] = None, menu: Option[Html] = None, moreJs: Html = emptyHtml, formCss: Boolean = false, openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
@base.layout(
title = title,
side = side,
menu = menu,
moreCss = cssTag("forum.css"),
moreCss = cssTags(Map("forum.css" -> true, "form3.css" -> formCss)),
moreJs = moreJs,
openGraph = openGraph) {
<div id="lichess_forum" class="content_box no_padding">

View File

@ -1,11 +1,4 @@
@(text: Field, topic: Option[lila.forum.Topic])(implicit ctx: Context)
<label>
<span class="required">@trans.message()</span>
<div>
@topic match {
case Some(topic) => { <textarea class="post-text-area" name="@text.name" data-topic="@topic.id">@text.value</textarea> }
case None => { <textarea class="post-text-area" name="@text.name">@text.value</textarea> }
}
@errMsg(text)
</div>
</label>
@form3.group(text, trans.message()) { f =>
@form3.textarea(f, klass = "post-text-area", rows = 10.some, required = true, attrs = topic.??(t => s"""data-topic="${t.id}""""))
}

View File

@ -1,6 +1,6 @@
@(categ: lila.forum.Categ, form: Form[_], captcha: lila.common.Captcha)(implicit ctx: Context)
@forum.layout(title = "New forum topic", moreJs=jsTag("forum-post.js")) {
@forum.layout(title = "New forum topic", moreJs = jsTag("forum-post.js"), formCss = true) {
<ol class="crumbs">
<li><a style="text-decoration:none" data-icon="d" class="is4" href="@routes.ForumCateg.index"> Forum</a></li>
<li><h1>@categ.name</h1></li>
@ -17,21 +17,18 @@
</p>
</div>
<form class="wide" action="@routes.ForumTopic.create(categ.slug)" method="POST" novalidate>
<label>
<span class="required">@trans.subject()</span>
<div>
<input class="subject" autofocus="true" minlength=3 maxlength=100 value="@form("name").value" type="text" name="@form("name").name" id="@form("name").id">
@errMsg(form("name"))
</div>
</label>
<form class="form3 create-topic" action="@routes.ForumTopic.create(categ.slug)" method="POST" novalidate>
@form3.group(form("name"), trans.subject()) { f =>
@form3.input(f, autofocus = true, required = true, minLength = 3, maxLength = 100)
}
@forum.post.formFields(form("post")("text"), None)
@base.captcha(form("post"), captcha)
@errMsg(form("post"))
<button class="submit button" type="submit" data-icon="E"> @trans.createTheTopic()</button>
@form3.actions {
<a href="@routes.ForumCateg.show(categ.slug)">@trans.cancel()</a>
@if(isGranted(_.PublicMod)){
<button class="button" type="submit" data-icon="" name="post.modIcon" value="true"> Create topic as mod</button>
@form3.submit(Html("Create as mod"), nameValue = ("post.modIcon", "true").some, icon = "".some)
}
@form3.submit(trans.createTheTopic())
}
<a href="@routes.ForumCateg.show(categ.slug)" style="margin-left:20px">@trans.cancel()</a>
</form>
}

View File

@ -5,20 +5,17 @@
@jsTag("embed-analyse.js")
}
@modMenu = {
@mod.menu("forum")
}
@title = @{ s"${topic.name} • page ${posts.currentPage}/${posts.nbPages} • ${categ.name}" }
@forum.layout(
title = title,
menu = modMenu.some.ifTrue(categ.isStaff),
menu = categ.isStaff.option(mod.menu("forum")),
moreJs = moreJs,
openGraph = lila.app.ui.OpenGraph(
title = topic.name,
url = s"$netBaseUrl${routes.ForumTopic.show(categ.slug, topic.slug, posts.currentPage).url}",
description = shorten(posts.currentPageResults.headOption.??(_.text), 152)).some) {
description = shorten(posts.currentPageResults.headOption.??(_.text), 152)).some,
formCss = formWithCaptcha.isDefined) {
<div class="topic">
@categ.team.map { team =>
<ol class="crumbs">
@ -109,15 +106,16 @@ description = shorten(posts.currentPageResults.headOption.??(_.text), 152)).some
@formWithCaptcha.map {
case (form, captcha) => {
<h2 class="postNewTitle" id="reply">@trans.replyToThisTopic()</h2>
<form class="wide" action="@routes.ForumPost.create(categ.slug, topic.slug, posts.currentPage)#reply" method="POST" novalidate>
<form class="form3 reply" action="@routes.ForumPost.create(categ.slug, topic.slug, posts.currentPage)#reply" method="POST" novalidate>
@forum.post.formFields(form("text"), topic.some)
@base.captcha(form, captcha)
@globalError(form)
<button type="submit" class="submit button" data-icon="E"> @trans.reply()</button>
@form3.actions {
<a href="@routes.ForumCateg.show(categ.slug)">@trans.cancel()</a>
@if(isGranted(_.PublicMod)){
<button type="submit" class="button" data-icon="" name="modIcon" value="true" /> Reply as mod</button>
@form3.submit(Html("Reply as mod"), nameValue = ("post.modIcon", "true").some, icon = "".some)
}
@form3.submit(trans.reply())
}
<a href="@routes.ForumCateg.show(categ.slug)" style="margin-left:20px">@trans.cancel()</a>
</form>
}
}.getOrElse {

View File

@ -28,7 +28,5 @@ object Captcha {
val failMessage = "captcha.fail"
def isFailed(form: Form.FormLike) =
form.errors.exists { e =>
e.key == "" && e.messages.has(failMessage)
}
form.errors.exists { _.messages has failMessage }
}

View File

@ -23,9 +23,9 @@ $(function() {
$('.post-text-area').one('focus', function() {
var textarea = this;
var textarea = this, topicId = $(this).attr('data-topic');
lichess.loadScript('vendor/textcomplete.min.js').then(function() {
if (topicId) lichess.loadScript('vendor/textcomplete.min.js').then(function() {
var searchCandidates = function(term, candidateUsers) {
return candidateUsers.filter(function(user) {
@ -37,7 +37,7 @@ $(function() {
// forums will be only to read the thread. So the 'thread participants' starts out empty until the post text area
// is focused.
var threadParticipants = $.ajax({
url: "/forum/participants/" + $('.post-text-area').attr('data-topic')
url: "/forum/participants/" + topicId
});
var textcomplete = new Textcomplete(new Textcomplete.editors.Textarea(textarea));

View File

@ -231,45 +231,14 @@ div.category .description {
font-style: italic;
}
#lichess_forum form.wide {
margin-bottom: 1em;
#lichess_forum form.create-topic {
margin: 20px;
}
#lichess_forum form.wide {
#lichess_forum form.reply {
margin-top: 2em;
font-size: 1.3em;
}
#lichess_forum form.wide label {
display: flex;
margin-bottom: 2em;
}
#lichess_forum form.wide label span {
width: 100px;
text-align: right;
margin: 8px 10px 0 0;
}
#lichess_forum form.wide label span.required:after {
content: "*";
color: #FF6666;
margin-left: 3px;
}
#lichess_forum form.wide input.subject {
width: 500px;
}
#lichess_forum form.wide textarea {
width: 500px;
height: 150px;
}
#lichess_forum form.wide input.author {
width: 300px;
}
#lichess_forum form.wide .submit {
margin-left: 110px;
}
#lichess_forum form.wide p.error {
color: red;
#lichess_forum form.reply label[for=form3-text] {
display: none;
}
div.pagination a, div.pagination span {