--- 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:_*))
}
}
--- 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] = {
--- 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
--- 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[_] =>
--- /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 <tzeman@volny.cz>
+ *
+ * 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 = <button>{S ? "Save"}</button>
+ 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 = <button>{S ? "Delete"}</button>
+ 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:
--- /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 <tzeman@volny.cz>
+ *
+ * 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:
--- /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 <tzeman@volny.cz>
+ *
+ * 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: <code>/templates-hidden/datatable</code>.
+ */
+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:
--- /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 <tzeman@volny.cz>
+ *
+ * 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.
+ * <code>item/view</code>.
+ */
+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:
--- 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 = <table>{attrs.map(_())}</table>
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 <code>/templates-hidden/panel</code> 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:
--- 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 = <a href={href}>{in}</a>
}
--- /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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+ <title>Entity View</title>
+ </head>
+ <body class="lift:content_id=main">
+ <div id="main" class="lift:surround?with=default;at=content">
+ <div class="span-24 last">
+ <h2><span class="lift:Menu.title"/></h2>
+ </div>
+ <div class="span-24 last">
+ <span class="lift:form"/>
+ </div>
+ </div>
+ </body>
+</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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+ <title>Entity List</title>
+ </head>
+ <body class="lift:content_id=main">
+ <div id="main" class="lift:surround?with=default;at=content">
+ <div class="span-24 last">
+ <h2><span class="lift:Menu.title"/></h2>
+ </div>
+ <div class="span-24 last">
+ <span class="lift:list"/>
+ </div>
+ </div>
+ </body>
+</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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+ <title>Entity View</title>
+ </head>
+ <body class="lift:content_id=main">
+ <div id="main" class="lift:surround?with=default;at=content">
+ <div class="span-24 last">
+ <h2><span class="lift:Menu.title"/></h2>
+ </div>
+ <div class="span-24 last">
+ <span class="lift:panel"/>
+ </div>
+ </div>
+ </body>
+</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 @@
<hr />
<div class="column span-24 last">
<span class="lift:Menubar"></span>
- <div class="lift:Msgs?showAll=true"></div>
</div>
+ <div class="lift:Msgs?showAll=false column span-24 last"></div>
<div class="span-10 last lift:action-links.are">
<span class="lift:action-links"/>
</div>
--- /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 @@
+<table>
+ <tr class="row">
+ <td class="n attr-name">Attribute</td>
+ <td class="v attr-value">Value</td>
+ </tr>
+</table>
--- /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 @@
+<div>
+ <wizard:screen_info>
+ <div>Page <wizard:screen_number/> of <wizard:total_screens/></div>
+ </wizard:screen_info>
+ <wizard:wizard_top> <div> <wizard:bind/> </div> </wizard:wizard_top>
+ <wizard:screen_top> <div> <wizard:bind/> </div> </wizard:screen_top>
+ <wizard:errors>
+ <div>
+ <ul>
+ <wizard:item><li><wizard:bind/></li></wizard:item>
+ </ul>
+ </div>
+ </wizard:errors>
+ <div>
+ <wizard:fields>
+ <table>
+ <tbody>
+ <tr lift:bind="wizard:line">
+ <td class="form-name">
+ <wizard:label>
+ <label wizard:for="">
+ <wizard:bind></wizard:bind>
+ </label>
+ </wizard:label>
+ <wizard:help>
+ <span>
+ <wizard:bind></wizard:bind>
+ </span>
+ </wizard:help>
+ <wizard:field_errors>
+ <ul>
+ <wizard:error>
+ <li><wizard:bind></wizard:bind></li>
+ </wizard:error>
+ </ul>
+ </wizard:field_errors>
+ </td>
+ <td class="form-value">
+ <wizard:form></wizard:form>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </wizard:fields>
+ </div>
+ <div>
+ <table>
+ <tr>
+ <td><wizard:prev></wizard:prev></td>
+ <td><wizard:cancel></wizard:cancel></td>
+ <td><wizard:next></wizard:next></td>
+ </tr>
+ </table>
+ </div>
+ <wizard:screen_bottom>
+ <div><wizard:bind></wizard:bind></div>
+ </wizard:screen_bottom>
+ <wizard:wizard_bottom>
+ <div><wizard:bind></wizard:bind></div>
+ </wizard:wizard_bottom>
+</div>