diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index aea3c6f..32982f4 100644 --- a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -10,8 +10,8 @@ trait JsonMessageCatalogue extends MessageCatalogue: def messages: js.Dictionary[String] - override def apply(id: MessageId): Option[String] = + override def get(id: MessageId): Option[String] = messages.get(id.toString) - override def apply(msg: UserMessage): Option[String] = - apply(msg.id).map(_.format(msg.args: _*)) + override def get(msg: UserMessage): Option[String] = + get(msg.id).map(_.format(msg.args: _*)) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index aea3c6f..32982f4 100644 --- a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -10,8 +10,8 @@ trait JsonMessageCatalogue extends MessageCatalogue: def messages: js.Dictionary[String] - override def apply(id: MessageId): Option[String] = + override def get(id: MessageId): Option[String] = messages.get(id.toString) - override def apply(msg: UserMessage): Option[String] = - apply(msg.id).map(_.format(msg.args: _*)) + override def get(msg: UserMessage): Option[String] = + get(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala index ac00bcc..2764124 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala @@ -7,23 +7,23 @@ import java.time.Instant trait HtmlRenderable[A]: - def toHtml(a: A): Modifier[HtmlElement] - extension (a: A) def render: Modifier[HtmlElement] = toHtml(a) + def toHtml(a: A): Node + extension (a: A) def render: Node = toHtml(a) object HtmlRenderable: given elementValue: HtmlRenderable[HtmlElement] with - def toHtml(a: HtmlElement): Modifier[HtmlElement] = a + def toHtml(a: HtmlElement): Node = a given stringValue: HtmlRenderable[String] with - def toHtml(v: String): Modifier[HtmlElement] = + def toHtml(v: String): Node = com.raquo.laminar.nodes.TextNode(v) given dateValue: HtmlRenderable[LocalDate] with - def toHtml(v: LocalDate): Modifier[HtmlElement] = + def toHtml(v: LocalDate): Node = TimeUtils.formatDate(v) given instantValue: HtmlRenderable[Instant] with - def toHtml(v: Instant): Modifier[HtmlElement] = + def toHtml(v: Instant): Node = TimeUtils.formatDateTime(v) given plainMultiLineValue: HtmlRenderable[PlainMultiLine] with - def toHtml(v: PlainMultiLine): Modifier[HtmlElement] = + def toHtml(v: PlainMultiLine): Node = p( v.split("\n") .map(t => Seq(com.raquo.laminar.nodes.TextNode(t), br())) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index aea3c6f..32982f4 100644 --- a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -10,8 +10,8 @@ trait JsonMessageCatalogue extends MessageCatalogue: def messages: js.Dictionary[String] - override def apply(id: MessageId): Option[String] = + override def get(id: MessageId): Option[String] = messages.get(id.toString) - override def apply(msg: UserMessage): Option[String] = - apply(msg.id).map(_.format(msg.args: _*)) + override def get(msg: UserMessage): Option[String] = + get(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala index ac00bcc..2764124 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala @@ -7,23 +7,23 @@ import java.time.Instant trait HtmlRenderable[A]: - def toHtml(a: A): Modifier[HtmlElement] - extension (a: A) def render: Modifier[HtmlElement] = toHtml(a) + def toHtml(a: A): Node + extension (a: A) def render: Node = toHtml(a) object HtmlRenderable: given elementValue: HtmlRenderable[HtmlElement] with - def toHtml(a: HtmlElement): Modifier[HtmlElement] = a + def toHtml(a: HtmlElement): Node = a given stringValue: HtmlRenderable[String] with - def toHtml(v: String): Modifier[HtmlElement] = + def toHtml(v: String): Node = com.raquo.laminar.nodes.TextNode(v) given dateValue: HtmlRenderable[LocalDate] with - def toHtml(v: LocalDate): Modifier[HtmlElement] = + def toHtml(v: LocalDate): Node = TimeUtils.formatDate(v) given instantValue: HtmlRenderable[Instant] with - def toHtml(v: Instant): Modifier[HtmlElement] = + def toHtml(v: Instant): Node = TimeUtils.formatDateTime(v) given plainMultiLineValue: HtmlRenderable[PlainMultiLine] with - def toHtml(v: PlainMultiLine): Modifier[HtmlElement] = + def toHtml(v: PlainMultiLine): Node = p( v.split("\n") .map(t => Seq(com.raquo.laminar.nodes.TextNode(t), br())) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala index cb405e9..ea6891f 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala @@ -9,71 +9,71 @@ import works.iterative.ui.components.tailwind.form.ActionButtons import works.iterative.ui.components.tailwind.HtmlComponent import works.iterative.ui.components.tailwind.form.ActionButton +import works.iterative.ui.components.tailwind.ComponentContext + +type ValueContent = String | Node +type OptionalValueContent = ValueContent | Option[ValueContent] + +case class LabeledValue(label: String, body: OptionalValueContent): + def content: Option[Node] = body match + case Some(s: String) => Some(s) + case Some(m: Node) => Some(m) + case s: String => Some(s) + case m: Node => Some(m) + case _ => None + +object LabeledValue: + given renderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, V), LabeledValue] with + def apply(v: (String, V)) = + LabeledValue(cctx.messages(v._1), Some(v._2.render)) + + given optionalRenderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, Option[V]), LabeledValue] with + def apply(v: (String, Option[V])) = + LabeledValue(cctx.messages(v._1), v._2.map(_.render)) // TODO: drop UI string, use MessageId, use builder like FormBuilder case class LeftAlignedInCard[A]( title: String, subtitle: String, - data: List[LeftAlignedInCard.OptionalLabeledValue], - // TODO: a version without actions - actions: List[ActionButton[A]] -) + data: List[LabeledValue], + actions: Option[Modifier[HtmlElement]] +): -object LeftAlignedInCard: - case class OptionalLabeledValue( - label: UIString, - v: Option[Modifier[HtmlElement]] - ) - - trait AsValue[V]: - def toLabeled(n: UIString, v: V): OptionalLabeledValue - extension (v: V) - def labeled(n: UIString): OptionalLabeledValue = toLabeled(n, v) - - given optionValue[V: HtmlRenderable]: AsValue[Option[V]] with - def toLabeled(n: UIString, v: Option[V]): OptionalLabeledValue = - OptionalLabeledValue(n, v.map(_.render)) - - given [V: HtmlRenderable]: AsValue[V] with - def toLabeled(n: UIString, v: V): OptionalLabeledValue = - OptionalLabeledValue(n, Some(v.render)) - - given leftAlignedInCardComponent[A](using - HtmlComponent[_, ActionButtons[A]] - ): BaseHtmlComponent[LeftAlignedInCard[A]] = - (d: LeftAlignedInCard[A]) => + private def renderDataRow(value: LabeledValue): Option[HtmlElement] = + value.content.map(c => div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3(cls := "text-lg leading-6 font-medium text-gray-900", d.title), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", d.subtitle) - ), + cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", + dt(cls := "text-sm font-medium text-gray-500", value.label), + dd( + cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", + c + ) + ) + ) + + def element: HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3(cls := "text-lg leading-6 font-medium text-gray-900", title), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", subtitle) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:p-0", + dl( + cls := "sm:divide-y sm:divide-gray-200", + data.map(renderDataRow).collect { case Some(el) => el } + ) + ), + actions.map(acts => div( cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - dl( - cls := "sm:divide-y sm:divide-gray-200", - d.data.collect { case OptionalLabeledValue(label, Some(body)) => - div( - cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", - dt(cls := "text-sm font-medium text-gray-500", label), - dd( - cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", - body - ) - ) - } - ) - ), - if d.actions.nonEmpty then - Some( - div( - cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - div( - cls := "px-4 py-5 sm:px-6", - ActionButtons(d.actions).element - ) - ) - ) - else None + div(cls := "px-4 py-5 sm:px-6", acts) + ) ) + ) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index aea3c6f..32982f4 100644 --- a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -10,8 +10,8 @@ trait JsonMessageCatalogue extends MessageCatalogue: def messages: js.Dictionary[String] - override def apply(id: MessageId): Option[String] = + override def get(id: MessageId): Option[String] = messages.get(id.toString) - override def apply(msg: UserMessage): Option[String] = - apply(msg.id).map(_.format(msg.args: _*)) + override def get(msg: UserMessage): Option[String] = + get(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala index ac00bcc..2764124 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala @@ -7,23 +7,23 @@ import java.time.Instant trait HtmlRenderable[A]: - def toHtml(a: A): Modifier[HtmlElement] - extension (a: A) def render: Modifier[HtmlElement] = toHtml(a) + def toHtml(a: A): Node + extension (a: A) def render: Node = toHtml(a) object HtmlRenderable: given elementValue: HtmlRenderable[HtmlElement] with - def toHtml(a: HtmlElement): Modifier[HtmlElement] = a + def toHtml(a: HtmlElement): Node = a given stringValue: HtmlRenderable[String] with - def toHtml(v: String): Modifier[HtmlElement] = + def toHtml(v: String): Node = com.raquo.laminar.nodes.TextNode(v) given dateValue: HtmlRenderable[LocalDate] with - def toHtml(v: LocalDate): Modifier[HtmlElement] = + def toHtml(v: LocalDate): Node = TimeUtils.formatDate(v) given instantValue: HtmlRenderable[Instant] with - def toHtml(v: Instant): Modifier[HtmlElement] = + def toHtml(v: Instant): Node = TimeUtils.formatDateTime(v) given plainMultiLineValue: HtmlRenderable[PlainMultiLine] with - def toHtml(v: PlainMultiLine): Modifier[HtmlElement] = + def toHtml(v: PlainMultiLine): Node = p( v.split("\n") .map(t => Seq(com.raquo.laminar.nodes.TextNode(t), br())) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala index cb405e9..ea6891f 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala @@ -9,71 +9,71 @@ import works.iterative.ui.components.tailwind.form.ActionButtons import works.iterative.ui.components.tailwind.HtmlComponent import works.iterative.ui.components.tailwind.form.ActionButton +import works.iterative.ui.components.tailwind.ComponentContext + +type ValueContent = String | Node +type OptionalValueContent = ValueContent | Option[ValueContent] + +case class LabeledValue(label: String, body: OptionalValueContent): + def content: Option[Node] = body match + case Some(s: String) => Some(s) + case Some(m: Node) => Some(m) + case s: String => Some(s) + case m: Node => Some(m) + case _ => None + +object LabeledValue: + given renderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, V), LabeledValue] with + def apply(v: (String, V)) = + LabeledValue(cctx.messages(v._1), Some(v._2.render)) + + given optionalRenderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, Option[V]), LabeledValue] with + def apply(v: (String, Option[V])) = + LabeledValue(cctx.messages(v._1), v._2.map(_.render)) // TODO: drop UI string, use MessageId, use builder like FormBuilder case class LeftAlignedInCard[A]( title: String, subtitle: String, - data: List[LeftAlignedInCard.OptionalLabeledValue], - // TODO: a version without actions - actions: List[ActionButton[A]] -) + data: List[LabeledValue], + actions: Option[Modifier[HtmlElement]] +): -object LeftAlignedInCard: - case class OptionalLabeledValue( - label: UIString, - v: Option[Modifier[HtmlElement]] - ) - - trait AsValue[V]: - def toLabeled(n: UIString, v: V): OptionalLabeledValue - extension (v: V) - def labeled(n: UIString): OptionalLabeledValue = toLabeled(n, v) - - given optionValue[V: HtmlRenderable]: AsValue[Option[V]] with - def toLabeled(n: UIString, v: Option[V]): OptionalLabeledValue = - OptionalLabeledValue(n, v.map(_.render)) - - given [V: HtmlRenderable]: AsValue[V] with - def toLabeled(n: UIString, v: V): OptionalLabeledValue = - OptionalLabeledValue(n, Some(v.render)) - - given leftAlignedInCardComponent[A](using - HtmlComponent[_, ActionButtons[A]] - ): BaseHtmlComponent[LeftAlignedInCard[A]] = - (d: LeftAlignedInCard[A]) => + private def renderDataRow(value: LabeledValue): Option[HtmlElement] = + value.content.map(c => div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3(cls := "text-lg leading-6 font-medium text-gray-900", d.title), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", d.subtitle) - ), + cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", + dt(cls := "text-sm font-medium text-gray-500", value.label), + dd( + cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", + c + ) + ) + ) + + def element: HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3(cls := "text-lg leading-6 font-medium text-gray-900", title), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", subtitle) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:p-0", + dl( + cls := "sm:divide-y sm:divide-gray-200", + data.map(renderDataRow).collect { case Some(el) => el } + ) + ), + actions.map(acts => div( cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - dl( - cls := "sm:divide-y sm:divide-gray-200", - d.data.collect { case OptionalLabeledValue(label, Some(body)) => - div( - cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", - dt(cls := "text-sm font-medium text-gray-500", label), - dd( - cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", - body - ) - ) - } - ) - ), - if d.actions.nonEmpty then - Some( - div( - cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - div( - cls := "px-4 py-5 sm:px-6", - ActionButtons(d.actions).element - ) - ) - ) - else None + div(cls := "px-4 py-5 sm:px-6", acts) + ) ) + ) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala index 887abe9..e02fe4d 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala @@ -39,9 +39,7 @@ cls(style.text), cls(style.extra), cls(style.focus), - ctx - .messages(s"action.$name.title") - .getOrElse(s"action.$name.title"), + ctx.messages(s"action.$name.title"), onClick.mapTo(action) --> actions ) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala index 76e812a..ac38528 100644 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -5,5 +5,10 @@ // we need to be able to render HTML messages // like a list of items for example trait MessageCatalogue: - def apply(id: MessageId): Option[String] - def apply(msg: UserMessage): Option[String] + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index aea3c6f..32982f4 100644 --- a/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -10,8 +10,8 @@ trait JsonMessageCatalogue extends MessageCatalogue: def messages: js.Dictionary[String] - override def apply(id: MessageId): Option[String] = + override def get(id: MessageId): Option[String] = messages.get(id.toString) - override def apply(msg: UserMessage): Option[String] = - apply(msg.id).map(_.format(msg.args: _*)) + override def get(msg: UserMessage): Option[String] = + get(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala index ac00bcc..2764124 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/Renderable.scala @@ -7,23 +7,23 @@ import java.time.Instant trait HtmlRenderable[A]: - def toHtml(a: A): Modifier[HtmlElement] - extension (a: A) def render: Modifier[HtmlElement] = toHtml(a) + def toHtml(a: A): Node + extension (a: A) def render: Node = toHtml(a) object HtmlRenderable: given elementValue: HtmlRenderable[HtmlElement] with - def toHtml(a: HtmlElement): Modifier[HtmlElement] = a + def toHtml(a: HtmlElement): Node = a given stringValue: HtmlRenderable[String] with - def toHtml(v: String): Modifier[HtmlElement] = + def toHtml(v: String): Node = com.raquo.laminar.nodes.TextNode(v) given dateValue: HtmlRenderable[LocalDate] with - def toHtml(v: LocalDate): Modifier[HtmlElement] = + def toHtml(v: LocalDate): Node = TimeUtils.formatDate(v) given instantValue: HtmlRenderable[Instant] with - def toHtml(v: Instant): Modifier[HtmlElement] = + def toHtml(v: Instant): Node = TimeUtils.formatDateTime(v) given plainMultiLineValue: HtmlRenderable[PlainMultiLine] with - def toHtml(v: PlainMultiLine): Modifier[HtmlElement] = + def toHtml(v: PlainMultiLine): Node = p( v.split("\n") .map(t => Seq(com.raquo.laminar.nodes.TextNode(t), br())) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala index cb405e9..ea6891f 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/data_display/description_lists/LeftAlignedInCard.scala @@ -9,71 +9,71 @@ import works.iterative.ui.components.tailwind.form.ActionButtons import works.iterative.ui.components.tailwind.HtmlComponent import works.iterative.ui.components.tailwind.form.ActionButton +import works.iterative.ui.components.tailwind.ComponentContext + +type ValueContent = String | Node +type OptionalValueContent = ValueContent | Option[ValueContent] + +case class LabeledValue(label: String, body: OptionalValueContent): + def content: Option[Node] = body match + case Some(s: String) => Some(s) + case Some(m: Node) => Some(m) + case s: String => Some(s) + case m: Node => Some(m) + case _ => None + +object LabeledValue: + given renderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, V), LabeledValue] with + def apply(v: (String, V)) = + LabeledValue(cctx.messages(v._1), Some(v._2.render)) + + given optionalRenderableToLabeledValue[V: HtmlRenderable](using + cctx: ComponentContext + ): Conversion[(String, Option[V]), LabeledValue] with + def apply(v: (String, Option[V])) = + LabeledValue(cctx.messages(v._1), v._2.map(_.render)) // TODO: drop UI string, use MessageId, use builder like FormBuilder case class LeftAlignedInCard[A]( title: String, subtitle: String, - data: List[LeftAlignedInCard.OptionalLabeledValue], - // TODO: a version without actions - actions: List[ActionButton[A]] -) + data: List[LabeledValue], + actions: Option[Modifier[HtmlElement]] +): -object LeftAlignedInCard: - case class OptionalLabeledValue( - label: UIString, - v: Option[Modifier[HtmlElement]] - ) - - trait AsValue[V]: - def toLabeled(n: UIString, v: V): OptionalLabeledValue - extension (v: V) - def labeled(n: UIString): OptionalLabeledValue = toLabeled(n, v) - - given optionValue[V: HtmlRenderable]: AsValue[Option[V]] with - def toLabeled(n: UIString, v: Option[V]): OptionalLabeledValue = - OptionalLabeledValue(n, v.map(_.render)) - - given [V: HtmlRenderable]: AsValue[V] with - def toLabeled(n: UIString, v: V): OptionalLabeledValue = - OptionalLabeledValue(n, Some(v.render)) - - given leftAlignedInCardComponent[A](using - HtmlComponent[_, ActionButtons[A]] - ): BaseHtmlComponent[LeftAlignedInCard[A]] = - (d: LeftAlignedInCard[A]) => + private def renderDataRow(value: LabeledValue): Option[HtmlElement] = + value.content.map(c => div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3(cls := "text-lg leading-6 font-medium text-gray-900", d.title), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", d.subtitle) - ), + cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", + dt(cls := "text-sm font-medium text-gray-500", value.label), + dd( + cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", + c + ) + ) + ) + + def element: HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3(cls := "text-lg leading-6 font-medium text-gray-900", title), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", subtitle) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:p-0", + dl( + cls := "sm:divide-y sm:divide-gray-200", + data.map(renderDataRow).collect { case Some(el) => el } + ) + ), + actions.map(acts => div( cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - dl( - cls := "sm:divide-y sm:divide-gray-200", - d.data.collect { case OptionalLabeledValue(label, Some(body)) => - div( - cls := "py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6", - dt(cls := "text-sm font-medium text-gray-500", label), - dd( - cls := "mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2", - body - ) - ) - } - ) - ), - if d.actions.nonEmpty then - Some( - div( - cls := "border-t border-gray-200 px-4 py-5 sm:p-0", - div( - cls := "px-4 py-5 sm:px-6", - ActionButtons(d.actions).element - ) - ) - ) - else None + div(cls := "px-4 py-5 sm:px-6", acts) + ) ) + ) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala index 887abe9..e02fe4d 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/form/ActionButtons.scala @@ -39,9 +39,7 @@ cls(style.text), cls(style.extra), cls(style.focus), - ctx - .messages(s"action.$name.title") - .getOrElse(s"action.$name.title"), + ctx.messages(s"action.$name.title"), onClick.mapTo(action) --> actions ) diff --git a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/navigation/Tabs.scala b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/navigation/Tabs.scala index 4d76b79..5918215 100644 --- a/ui/components/src/main/scala/works/iterative/ui/components/tailwind/navigation/Tabs.scala +++ b/ui/components/src/main/scala/works/iterative/ui/components/tailwind/navigation/Tabs.scala @@ -30,7 +30,7 @@ option( defaultSelected <-- selected.map(t == _), value := t.toString, - ctx.messages(t).getOrElse(t.toString) + ctx.messages(t) ) }, onChange.mapToValue.map(m(_)) --> updates @@ -52,7 +52,7 @@ else "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" ), - ctx.messages(t).getOrElse(t.toString), + ctx.messages(t), onClick.preventDefault.mapTo(v) --> updates ) }