
net.liftweb.mapper.view.TableEditor.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2009-2011 WorldWide Conferencing, LLC
*
* 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.liftweb
package mapper
package view
import scala.xml.{NodeSeq, Text}
import common.Box
import util.Helpers
import Helpers._
import http.{SHtml, S, DispatchSnippet, js}
import S.?
import js.JsCmds.{Script, Run}
import Util._
/**
* Keeps track of pending adds to and removes from a list of mappers.
* Supports in-memory sorting by a field.
* Usage: override metaMapper with a MetaMapper instance, call sortBy
* to specify the field to sort by. If it is already sorted by that
* field it will sort descending, otherwise ascending.
* Call save to actualize changes.
* @author nafg
*/
trait ItemsList[T <: Mapper[T]] {
/**
* The MetaMapper that provides create and findAll functionality etc.
* Must itself be a T (the mapper type it represents)
*/
def metaMapper: T with MetaMapper[T]
/**
* Whether the sorting algorithm should put null first or last
*/
var sortNullFirst = true
/**
* The list of items that correspond to items in the database
*/
var current: List[T] = Nil
/**
* The list of items pending to be added to the database
*/
var added: List[T] = Nil
/**
* The list of items to be deleted from current
*/
var removed: List[T] = Nil
/**
* The field to sort by, if any
*/
var sortField: Option[MappedField[_, T]] = None
/**
* The sort direction
*/
var ascending = true
/**
* Returns the items (current + added - removed), sorted.
* Sorting sorts strings case-insensitive, as well as Ordered and java.lang.Comparable.
* Anything else where both values are nonnull are sorted via their toString method (case sensitive)
*/
def items: Seq[T] = {
val unsorted: List[T] = current.filterNot(removed.contains) ++ added
sortField match {
case None =>
unsorted
case Some(field) =>
unsorted.sortWith {
(a, b) => ((field.actualField(a).get: Any, field.actualField(b).get: Any) match {
case (aval: String, bval: String) => aval.toLowerCase < bval.toLowerCase
case (aval: Ordered[_], bval: Ordered[_]) =>
aval.asInstanceOf[Ordered[Any]] < bval.asInstanceOf[Ordered[Any]]
case (aval: java.lang.Comparable[_], bval: java.lang.Comparable[_]) =>
(aval.asInstanceOf[java.lang.Comparable[Any]] compareTo bval.asInstanceOf[java.lang.Comparable[Any]]) < 0
case (null, _) => sortNullFirst
case (_, null) => !sortNullFirst
case (aval, bval) => aval.toString < bval.toString
}) match {
case cmp =>
if(ascending) cmp else !cmp
}
}
}
}
/**
* Adds a new, unsaved item
*/
def add(): Unit = {
added ::= metaMapper.create
}
/**
* Marks an item pending for removal
*/
def remove(i: T): Unit = {
if(added.exists(i.eq))
added = added.filter(i.ne)
else if(current.contains(i))
removed ::= i
}
/**
* Reset the ItemsList from the database: calls refresh, and 'added' and 'removed' are cleared.
*/
def reload(): Unit = {
refresh()
added = Nil
removed = Nil
}
/**
* Reloads the contents of 'current' from the database
*/
def refresh(): Unit = {
current = metaMapper.findAll()
}
/**
* Sends to the database:
* added is saved
* removed is deleted
* (current - removed) is saved
*/
def save(): Unit = {
val (successAdd, failAdd) = added.partition(_.save)
added = failAdd
val (successRemove, failRemove) = removed.partition(_.delete_!)
current = current.filterNot(successRemove.contains)
removed = failRemove
for(c <- current if c.validate.isEmpty) c.save
current ++= successAdd
}
def sortBy(field: MappedField[_, T]): Unit = (sortField, ascending) match {
case (Some(f), true) if f eq field =>
ascending = false
case _ | null =>
sortField = Some(field)
ascending = true
}
def sortFn(field: MappedField[_, T]): () => Unit = (sortField, ascending) match {
case (Some(f), true) if f eq field =>
() => ascending = false
case _ | null =>
() => {
sortField = Some(field)
ascending = true
}
}
reload()
}
/**
* Holds a registry of TableEditor delegates
* Call TableEditor.registerTable(name_to_use_in_view, meta_mapper_for_the_table, display_title)
* in Boot after DB.defineConnectionManager.
* Referencing TableEditor triggers registering its snippet package and enabling
* the provided template, /tableeditor/default.
* @author nafg
*/
object TableEditor {
net.liftweb.http.LiftRules.addToPackages("net.liftweb.mapper.view")
private[view] val map = new scala.collection.mutable.HashMap[String, TableEditorImpl[_]]
def registerTable[T<:Mapper[T]](name: String, meta: T with MetaMapper[T], title: String): Unit =
map(name) = new TableEditorImpl(title, meta)
}
package snippet {
/**
* This is the snippet that the view references.
* It requires the following contents:
* table:title - the title registered in Boot
* header:fields - repeated for every field of the MetaMapper, for the header.
* field:name - the displayName of the field, capified. Links to sort by the field.
* table:items - repeated for each record
* item:fields - repeated for each field of the current record
* field:form - the result of toForm on the field
* item:removeBtn - a button to remove the current item
* table:insertBtn - a button to insert another item
* For a default layout, use lift:embed what="/tableeditor/default", with
* @author nafg
*/
class TableEditor extends DispatchSnippet {
private def getInstance: Box[TableEditorImpl[_]] = S.attr("table").map(TableEditor.map(_))
def dispatch: DispatchIt = {
case "edit" =>
val o = getInstance.openOrThrowException("if we don't have the table attr, we want the dev to know about it.")
o.edit
}
}
}
/**
* This class does the actual view binding against a ItemsList.
* The implementation is in the base trait ItemsListEditor
* @author nafg
*/
protected class TableEditorImpl[T <: Mapper[T]](val title: String, meta: T with MetaMapper[T]) extends ItemsListEditor[T] {
var items: ItemsList[T] = new ItemsList[T] {
def metaMapper: T with MetaMapper[T] = meta
}
}
/**
* General trait to edit an ItemsList.
* @author nafg
*/
trait ItemsListEditor[T<:Mapper[T]] {
def items: ItemsList[T]
def title: String
def onInsert(): Unit = items.add()
def onRemove(item: T): Unit = items.remove(item)
def onSubmit(): Unit = try {
items.save()
} catch {
case e: java.sql.SQLException =>
S.error("Not all items could be saved!")
}
def sortFn(f: MappedField[_, T]): ()=>Unit = items.sortFn(f)
val fieldFilter: MappedField[_,T]=>Boolean = (f: MappedField[_,T])=>true
def customBind(item: T): NodeSeq=>NodeSeq = (ns: NodeSeq) => ns
def edit: (NodeSeq)=>NodeSeq = {
def unsavedScript = ({Script(Run("""
var safeToContinue = false
window.onbeforeunload = function(evt) {{ // thanks Tim!
if(!safeToContinue) {{
var reply = "You have unsaved changes!";
if(typeof evt == 'undefined') evt = window.event;
if(evt) evt.returnValue = reply;
return reply;
}}
}}
"""))})
val noPrompt = "onclick" -> "safeToContinue=true"
val optScript = if(
(items.added.length + items.removed.length == 0) &&
items.current.forall(!_.dirty_?)
) {
NodeSeq.Empty
} else {
unsavedScript
}
val bindRemovedItems =
items.removed.map { item =>
"^" #> customBind(item) andThen
".fields" #> eachField(item, { f: MappedField[_, T] => ".form" #> {f.asHtml} }) &
".removeBtn" #> SHtml.submit(?("Remove"), ()=>onRemove(item), noPrompt) &
".msg" #> Text(?("Deleted"))
}
val bindRegularItems =
items.items.map { item =>
"^" #> customBind(item) andThen
".fields" #> eachField(item, { f: MappedField[_, T] => ".form" #> f.toForm }) &
".removeBtn" #> SHtml.submit(?("Remove"), ()=>onRemove(item), noPrompt) &
".msg" #> {
item.validate match {
case Nil =>
if (! item.saved_?)
Text(?("New"))
else if (item.dirty_?)
Text(?("Unsaved"))
else
NodeSeq.Empty
case errors =>
{errors.flatMap(e => - {e.msg}
)}
}
}
}
"^ >*" #> optScript andThen
".fields *" #> {
eachField[T](
items.metaMapper,
{ f: MappedField[_, T] =>
".name" #> SHtml.link(S.uri, sortFn(f), Text(capify(f.displayName)))
},
fieldFilter
)
} &
".table" #> {
".title *" #> title &
".insertBtn" #> SHtml.submit(?("Insert"), onInsert _, noPrompt) &
".item" #> (bindRegularItems ++ bindRemovedItems) &
".saveBtn" #> SHtml.submit(?("Save"), onSubmit _, noPrompt)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy