# HG changeset patch # User Tomas Zeman # Date 1328863989 -3600 # Node ID 995184977e9b1864746cbd0281c6d4fab218f9f3 # Parent 5a2f996a5ba00f1387534178bd49bb50608c46a2 a3e3bd30f0d19526 Contact UI diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/bootstrap/liftweb/Boot.scala --- a/src/main/scala/bootstrap/liftweb/Boot.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/bootstrap/liftweb/Boot.scala Fri Feb 10 09:53:09 2012 +0100 @@ -16,9 +16,11 @@ package bootstrap.liftweb import fis.base.model._ +import fis.crm.ui.ContactSnippet import net.liftweb.common._ import net.liftweb.db.{DB, ConnectionIdentifier} import net.liftweb.http._ +import net.liftweb.sitemap._ import net.liftweb.squerylrecord.SquerylRecord import net.liftweb.util._ import net.liftweb.util.Helpers._ @@ -39,6 +41,12 @@ super.boot + import Loc._ + + val menus = List(Menu("/", "FIS Main page") / "index" >> Hidden, + Menu.i("Home") / "" , ContactSnippet.menu) + + LiftRules.setSiteMap(SiteMap(menus:_*)) } } diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/fis/base/model/BaseSchema.scala --- a/src/main/scala/fis/base/model/BaseSchema.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/fis/base/model/BaseSchema.scala Fri Feb 10 09:53:09 2012 +0100 @@ -32,6 +32,8 @@ override def tableNameFromClass(c: Class[_]): String = snakify(c.getSimpleName) + override def columnNameFromPropertyName(n: String): String = snakify(n) + protected def entityTable[T <: Entity[_]](name: String)(implicit manifestT: Manifest[T]): Table[T] = { diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/fis/base/model/Entity.scala --- a/src/main/scala/fis/base/model/Entity.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/fis/base/model/Entity.scala Fri Feb 10 09:53:09 2012 +0100 @@ -18,6 +18,7 @@ import net.liftweb.record.{MetaRecord, Record} import net.liftweb.record.field._ import net.liftweb.squerylrecord.KeyedRecord +import net.tz.lift.model.OptionalFieldDisplay import org.squeryl.annotations.Column /** @@ -28,6 +29,7 @@ val idField = new LongField(this.asInstanceOf[OwnerType]) val name = new StringField(this.asInstanceOf[OwnerType], "") val note = new OptionalTextareaField(this.asInstanceOf[OwnerType], 10240) + with OptionalFieldDisplay /** Display representation of this entity. */ def linkName = name.get diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/fis/crm/model/Contact.scala --- a/src/main/scala/fis/crm/model/Contact.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/fis/crm/model/Contact.scala Fri Feb 10 09:53:09 2012 +0100 @@ -16,8 +16,25 @@ package fis.crm.model import fis.base.model.{Entity, EntityUnion, MetaEntity} +import net.liftweb.common._ +import net.liftweb.http.S import net.liftweb.record.{MetaRecord, Record} import net.liftweb.record.field._ +import net.liftweb.util.FieldError +import net.tz.lift.model.OptionalFieldDisplay +import scala.xml.Text + +class OptionalEmailField[OwnerType <: Record[OwnerType]](rec: OwnerType, + maxLength: Int) extends OptionalStringField[OwnerType](rec, maxLength) { + override def validations = validateEmail _ :: Nil + protected def validateEmail(email: ValueType): List[FieldError] = + toBoxMyType(email) match { + case Empty | Full("") => Nil + case Full(v) if EmailField.validEmailAddr_?(v) => Nil + case _ => Text(S.??("invalid.email.address")) + } +} + class Contact private() extends Record[Contact] with Entity[Contact] { @@ -25,26 +42,41 @@ val firstName = new StringField(this, 80, "") val lastName = new StringField(this, 80, "") - val position = new OptionalStringField(this, 40) + val position = new OptionalStringField(this, 40) with OptionalFieldDisplay val workMail = new EmailField(this, 256) - val privateMail = new OptionalEmailField(this, 256) - val otherMail = new OptionalEmailField(this, 256) + val privateMail = new OptionalEmailField(this, 256) with OptionalFieldDisplay + val otherMail = new OptionalEmailField(this, 256) with OptionalFieldDisplay val workMobile = new StringField(this, 40) - val privateMobile = new OptionalStringField(this, 40) - val otherMobile = new OptionalStringField(this, 40) - val workPhone = new OptionalStringField(this, 40) - val privatePhone = new OptionalStringField(this, 40) - val fax = new OptionalStringField(this, 40) + val privateMobile = new OptionalStringField(this, 40) with + OptionalFieldDisplay + val otherMobile = new OptionalStringField(this, 40) with OptionalFieldDisplay + val workPhone = new OptionalStringField(this, 40) with OptionalFieldDisplay + val privatePhone = new OptionalStringField(this, 40) with OptionalFieldDisplay + val fax = new OptionalStringField(this, 40) with OptionalFieldDisplay override def linkName = lastName.get + " " + firstName.get lazy val entities = CrmSchema.entityContacts.right(this) + + def fieldsForView = List(lastName, firstName, position, workMail, workMobile, + workPhone, privateMail, privateMobile, privatePhone, fax, + otherMail, otherMobile, note) } +import net.liftweb.squerylrecord.RecordTypeMode._ + object Contact extends Contact with MetaRecord[Contact] with MetaEntity[Contact] { def getTable = CrmSchema.contacts + def contacts: List[Contact] = from(getTable) (c => + select(c) orderBy(c.lastName asc, c.firstName asc) + ) toList + + def fieldsForList = List(position, workMail, + workMobile, note) + + def delete(c: Contact) = getTable.delete(c.id) } trait ContactsAware { self: Entity[_] => diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/fis/crm/ui/ContactSnippet.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/fis/crm/ui/ContactSnippet.scala Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,161 @@ +/* + * Copyright 2011 Tomas Zeman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fis.crm.ui + +import fis.crm.model._ +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.sitemap._ +import net.liftweb.util._ +import net.liftweb.util.Helpers._ +import net.tz.lift.snippet.{A, DataTable, LocTpl, TplPanel, SnippetHelpers} +import scala.xml.{Elem, NodeSeq, Text} + +sealed trait ContactLoc { + def title: String + def url: List[String] + def tpl: String +} + +case object ListContacts extends ContactLoc { + def title = "Contacts" + def url = Nil + def tpl = "entity/list" +} + +case object AddContact extends ContactLoc { + def title = "Add contact" + def url = List("add") + def tpl = "entity/form" +} + +case class ShowContact(c: Contact) extends ContactLoc { + def title = "Contact " + c.linkName + def url = List(c.id.toString) + def tpl = "entity/view" +} + +case class EditContact(c: Contact) extends ContactLoc { + def title = "Edit contact " + c.linkName + def url = List(c.id.toString, "edit") + def tpl = "entity/form" +} + +case class DeleteContact(c: Contact) extends ContactLoc { + def title = "Delete contact " + c.linkName + def url = List(c.id.toString, "delete") + def tpl = "entity/form" +} + +object AsContactLoc extends Loggable { + def apply(pars: List[String]): Box[ContactLoc] = { + logger.debug("Params: " + pars) + pars match { + case List("add") => Full(AddContact) + case AsLong(id) :: xs => Contact.findByKey(id) map { c => xs match { + case List("edit") => EditContact(c) + case List("delete") => DeleteContact(c) + case _ => ShowContact(c) + }} + case Nil => Full(ListContacts) + case _ => Empty + } + } +} + +object ContactSnippet extends SnippetHelpers { + import Loc._ + + val prefix = "contact" + + lazy val menu: Menu = Menu("contact.list", ListContacts.title) / "contacts" >> + EarlyResponse(() => Full(RedirectResponse(url(ListContacts)))) submenus( + Menu("contact.add", AddContact.title) / prefix / "add", + Menu.params[ContactLoc]("contact.edit", "Edit contact", parMenu.parser, + parMenu.encoder) / prefix / * / "edit" >> CalcValue(() => cur.is match { + case ShowContact(c) => Full(ShowContact(c)) + case _ => Empty + }) , + Menu.params[ContactLoc]("contact.delete", "Delete contact", parMenu.parser, + parMenu.encoder) / prefix / * / "delete" >> CalcValue(() => cur.is match { + case ShowContact(c) => Full(ShowContact(c)) + case _ => Empty + }) , + menu1) + + lazy val menu1: Menu = parMenu >> new DispatchLocSnippets { + def dispatch = { n => (n, cur.is) match { + case ("list", ListContacts) => listContacts + case ("panel", ShowContact(c)) => ContactPanel(c) + case ("form", EditContact(c)) => ContactForm(c).dispatch("") + case ("form", AddContact) => ContactForm(Contact.createRecord). + dispatch("") + case ("form", DeleteContact(c)) => DeleteContactForm(c).dispatch("") + }} + } >> LocTpl(_.tpl) >> Hidden + + lazy val parMenu = Menu.params[ContactLoc]("contact", + Loc.LinkText(l => Text(l.title)), + AsContactLoc(_) pass { _.foreach { cur(_) } }, _.url) / prefix / ** + + def url(l: ContactLoc) = parMenu.toLoc.calcHref(l) + + object cur extends RequestVar[ContactLoc](ListContacts) + + def listContacts: CssTr = { + val cols = Contact.fieldsForList.map { _.displayName } + val cells = Contact.contacts.map { c => + A(url(ShowContact(c)), Text(c.linkName)) :: + (Contact.fieldsForList.map { f => + c.fieldByName(f.name).map { _.asHtml } openOr NodeSeq.Empty + }.toList) }.toList + new DataTable((S ? "Full name") :: cols, cells) + } + + case class ContactPanel(c: Contact) extends TplPanel(c.fieldsForView.map + { f => (f.displayHtml, f.asHtml) }) + + case class ContactForm(c: Contact) extends LiftScreen { + object contact extends ScreenVar(c) + contact.is.fieldsForView.map { f => addFields(() => f) } + override def finishButton: Elem = + protected def finish() { + val c = contact.is + Contact.getTable.insertOrUpdate(c) + S.notice("Contact " + c.linkName + " saved") + S.redirectTo(url(ShowContact(c))) + () + } + } + + case class DeleteContactForm(c: Contact) extends LiftScreen { + val confirm = field("Really delete this contact?", false) + override def finishButton: Elem = + protected def finish() { + confirm.is match { + case true => + Contact.delete(c) + S.notice("Contact " + c.linkName + " deleted") + S.redirectTo(url(ListContacts)) + case false => + S.redirectTo(url(ShowContact(c))) + } + () + } + } +} + +// vim: set ts=2 sw=2 et: diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/net/tz/lift/model/OptionalFieldDisplay.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/net/tz/lift/model/OptionalFieldDisplay.scala Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,28 @@ +/* + * Copyright 2011 Tomas Zeman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.tz.lift.model + +import net.liftweb.record.OptionalTypedField +import scala.xml.Text + +/** + * Nice display of optional fields, ie. w/o Some(...) + */ +trait OptionalFieldDisplay { self: OptionalTypedField[_] => + override def asHtml = Text(get map { _.toString } getOrElse "") +} + +// vim: set ts=2 sw=2 et: diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/net/tz/lift/snippet/DataTable.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/net/tz/lift/snippet/DataTable.scala Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,34 @@ +/* + * Copyright 2011 Tomas Zeman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.tz.lift.snippet + +import net.liftweb.http.TemplateFinder +import net.liftweb.util.Helpers._ // CSS transforms +import scala.xml.NodeSeq + +/** + * Template driven table, compatible w/ datatables.net. + * Template location: /templates-hidden/datatable. + */ +class DataTable(cols: List[String], cells: List[List[NodeSeq]]) extends CssTr { + def apply(in: NodeSeq): NodeSeq = TemplateFinder.findAnyTemplate( + List("templates-hidden", "datatable")) map { xml => + (".field-name *" #> cols & + ".row *" #> cells.map { vals => "td *" #> vals })(xml) + } openOr NodeSeq.Empty +} + +// vim: set ts=2 sw=2 et: diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/net/tz/lift/snippet/Loc.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/net/tz/lift/snippet/Loc.scala Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,36 @@ +/* + * Copyright 2011 Tomas Zeman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.tz.lift.snippet + +import net.liftweb.http.TemplateFinder +import net.liftweb.sitemap.Loc._ +import scala.xml.NodeSeq + +/** + * Helper object for custom Loc template based on template path, eg. + * item/view. + */ +object LocTpl { + + def apply(path: String): Template = Template(() => + TemplateFinder.findAnyTemplate(path split "/" toList) openOr NodeSeq.Empty) + + def apply[T](f: T => String): ValueTemplate[T] = ValueTemplate[T]( + _.flatMap { v => TemplateFinder.findAnyTemplate(f(v) split "/" toList) + } openOr NodeSeq.Empty) +} + +// vim: set ts=2 sw=2 et: diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/net/tz/lift/snippet/Panel.scala --- a/src/main/scala/net/tz/lift/snippet/Panel.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/net/tz/lift/snippet/Panel.scala Fri Feb 10 09:53:09 2012 +0100 @@ -46,12 +46,28 @@ new Panel(fields.map(AttrRow(_))) } -class Panel(attrs: => Iterable[AttrRow]) extends Function1[NodeSeq, NodeSeq] -{ +class Panel(attrs: => Iterable[AttrRow]) extends CssTr { def apply(in: NodeSeq): NodeSeq = {attrs.map(_())}
def &(other: Function1[NodeSeq, NodeSeq]) = new Function1[NodeSeq, NodeSeq] { def apply(in: NodeSeq): NodeSeq = List(Panel.this, other) flatMap (_(in)) } } +import net.liftweb.http.TemplateFinder +import net.liftweb.util.Helpers._ // CSS transforms + +/** + * A panel using template in /templates-hidden/panel for + * values rendering. + */ +class TplPanel(cells: List[(NodeSeq, NodeSeq)]) extends CssTr { + def apply(in: NodeSeq): NodeSeq = TemplateFinder.findAnyTemplate( + List("templates-hidden", "panel")) map { xml => + (".row *" #> cells.map { r => + ".n *" #> r._1 & + ".v *" #> r._2 + })(xml) + } openOr NodeSeq.Empty +} + // vim: set ts=2 sw=2 et: diff -r 5a2f996a5ba0 -r 995184977e9b src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala --- a/src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala Fri Feb 10 09:53:09 2012 +0100 @@ -19,10 +19,15 @@ import scala.xml.{NodeSeq, Text} trait SnippetHelpers { + + type CssTr = (NodeSeq => NodeSeq) + def mkPath(prefix: String, l: String*) = (prefix :: l.toList) mkString ("/", "/", "") } +trait CssTr extends Function1[NodeSeq, NodeSeq] + class A(href: => String) extends Function1[NodeSeq, NodeSeq] { def apply(in: NodeSeq): NodeSeq = {in} } diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/entity/form.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/entity/form.html Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,19 @@ + + + + + Entity View + + +
+
+

+
+
+ +
+
+ + + + diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/entity/list.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/entity/list.html Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,19 @@ + + + + + Entity List + + +
+
+

+
+
+ +
+
+ + + + diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/entity/view.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/entity/view.html Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,19 @@ + + + + + Entity View + + +
+
+

+
+
+ +
+
+ + + + diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/templates-hidden/default.html --- a/src/main/webapp/templates-hidden/default.html Fri Feb 10 09:53:08 2012 +0100 +++ b/src/main/webapp/templates-hidden/default.html Fri Feb 10 09:53:09 2012 +0100 @@ -23,8 +23,8 @@
-
+
diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/templates-hidden/panel.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/templates-hidden/panel.html Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,6 @@ + + + + + +
AttributeValue
diff -r 5a2f996a5ba0 -r 995184977e9b src/main/webapp/templates-hidden/wizard-all.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/templates-hidden/wizard-all.html Fri Feb 10 09:53:09 2012 +0100 @@ -0,0 +1,61 @@ +
+ +
Page of
+
+
+
+ +
+
    +
  • +
+
+
+
+ + + + + + + + +
+ + + + + + + + + +
    + +
  • +
    +
+
+
+ +
+
+
+
+ + + + + + +
+
+ +
+
+ +
+
+