mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-02-15 20:33:26 +00:00
Provide email proposals from address book
This commit is contained in:
parent
c84a69aa9c
commit
d535130c9e
@ -1,10 +1,11 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import fs2.Stream
|
||||
import cats.implicits._
|
||||
import cats.effect.{Effect, Resource}
|
||||
import docspell.common._
|
||||
import docspell.store.{AddResult, Store}
|
||||
import docspell.store.records.{RCollective, RUser}
|
||||
import docspell.store.records.{RCollective, RContact, RUser}
|
||||
import OCollective._
|
||||
import docspell.backend.PasswordCrypt
|
||||
import docspell.store.queries.QCollective
|
||||
@ -30,6 +31,13 @@ trait OCollective[F[_]] {
|
||||
current: Password,
|
||||
newPass: Password
|
||||
): F[PassChangeResult]
|
||||
|
||||
def getContacts(
|
||||
collective: Ident,
|
||||
query: Option[String],
|
||||
kind: Option[ContactKind]
|
||||
): Stream[F, RContact]
|
||||
|
||||
}
|
||||
|
||||
object OCollective {
|
||||
@ -119,5 +127,13 @@ object OCollective {
|
||||
|
||||
store.transact(q)
|
||||
}
|
||||
|
||||
def getContacts(
|
||||
collective: Ident,
|
||||
query: Option[String],
|
||||
kind: Option[ContactKind]
|
||||
): Stream[F, RContact] =
|
||||
store.transact(QCollective.getContacts(collective, query, kind))
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -68,8 +68,12 @@ form:
|
||||
<img src="../img/mail-item-1.jpg">
|
||||
</div>
|
||||
|
||||
Then write the mail. Multiple recipients may be specified by
|
||||
separating their addresses by comma.
|
||||
Then write the mail. Multiple recipients may be specified. The input
|
||||
field shows completion proposals from all contacts in your address
|
||||
book (from organizations and persons). Choose an address by pressing
|
||||
*Enter* or by clicking a proposal from the list. The proposal list can
|
||||
be iterated by the *Up* and *Down* arrows. You can type in any
|
||||
address, of course, it doesn't need to match a proposal.
|
||||
|
||||
If you have multiple mail settings defined, you can choose in the top
|
||||
dropdown which account to use for sending.
|
||||
@ -77,7 +81,7 @@ dropdown which account to use for sending.
|
||||
The last checkbox allows to choose whether docspell should add all
|
||||
attachments of the item to the mail. If it is unchecked, no
|
||||
attachments will be added. It is currently not possible to pick
|
||||
attachments, it's all or nothing.
|
||||
specific attachments, it's all or nothing.
|
||||
|
||||
Clicking *Cancel* will delete the inputs and close the mail form, but
|
||||
clicking the envelope icon again, will only close the form without
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 162 KiB |
@ -652,6 +652,25 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ItemInsights"
|
||||
/sec/collective/contacts:
|
||||
get:
|
||||
tags: [ Collective ]
|
||||
summary: Return a list of contacts.
|
||||
description: |
|
||||
Return a list of all contacts available from the collectives
|
||||
address book.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/q"
|
||||
- $ref: "#/components/parameters/contactKind"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ContactList"
|
||||
/sec/user:
|
||||
get:
|
||||
tags: [ Collective ]
|
||||
@ -2182,6 +2201,16 @@ components:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
ContactList:
|
||||
description: |
|
||||
A list of contacts.
|
||||
required:
|
||||
- items
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Contact"
|
||||
Contact:
|
||||
description: |
|
||||
Contact information.
|
||||
@ -2487,3 +2516,11 @@ components:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
contactKind:
|
||||
name: kind
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
One of the available contact kinds.
|
||||
schema:
|
||||
type: string
|
||||
|
@ -0,0 +1,24 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import org.http4s.QueryParamDecoder
|
||||
import org.http4s.ParseFailure
|
||||
import docspell.common.ContactKind
|
||||
import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
|
||||
|
||||
object QueryParam {
|
||||
case class QueryString(q: String)
|
||||
|
||||
implicit val contactKindDecoder: QueryParamDecoder[ContactKind] =
|
||||
QueryParamDecoder[String].emap(str =>
|
||||
ContactKind.fromString(str).left.map(s => ParseFailure(str, s))
|
||||
)
|
||||
|
||||
implicit val queryStringDecoder: QueryParamDecoder[QueryString] =
|
||||
QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase))
|
||||
|
||||
|
||||
|
||||
object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
|
||||
|
||||
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
|
||||
}
|
@ -6,7 +6,7 @@ import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import docspell.restserver.http4s._
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
@ -39,6 +39,16 @@ object CollectiveRoutes {
|
||||
resp <- sett.toResponse()
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / "contacts" :? QueryParam.QueryOpt(q) +& QueryParam.ContactKindOpt(kind) =>
|
||||
for {
|
||||
res <- backend.collective
|
||||
.getContacts(user.account.collective, q.map(_.q), kind)
|
||||
.take(50)
|
||||
.compile
|
||||
.toList
|
||||
resp <- Ok(ContactList(res.map(Conversions.mkContact)))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
for {
|
||||
collDb <- backend.collective.find(user.account.collective)
|
||||
|
@ -39,6 +39,9 @@ case class Column(name: String, ns: String = "", alias: String = "") {
|
||||
def isIn(values: Seq[Fragment]): Fragment =
|
||||
f ++ fr"IN (" ++ commas(values) ++ fr")"
|
||||
|
||||
def isIn(frag: Fragment): Fragment =
|
||||
f ++ fr"IN (" ++ frag ++ fr")"
|
||||
|
||||
def isOrDiscard[A: Put](value: Option[A]): Fragment =
|
||||
value match {
|
||||
case Some(v) => is(v)
|
||||
|
@ -1,10 +1,12 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import fs2.Stream
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
import docspell.common.{Direction, Ident}
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.records.{RAttachment, RItem, RTag, RTagItem}
|
||||
import docspell.store.records._
|
||||
import docspell.common.ContactKind
|
||||
|
||||
object QCollective {
|
||||
|
||||
@ -50,4 +52,38 @@ object QCollective {
|
||||
} yield InsightData(n0, n1, n2.getOrElse(0), Map.from(n3))
|
||||
}
|
||||
|
||||
def getContacts(
|
||||
coll: Ident,
|
||||
query: Option[String],
|
||||
kind: Option[ContactKind]
|
||||
): Stream[ConnectionIO, RContact] = {
|
||||
val RO = ROrganization
|
||||
val RP = RPerson
|
||||
val RC = RContact
|
||||
|
||||
val orgCond = selectSimple(Seq(RO.Columns.oid), RO.table, RO.Columns.cid.is(coll))
|
||||
val persCond = selectSimple(Seq(RP.Columns.pid), RP.table, RP.Columns.cid.is(coll))
|
||||
val queryCond = query match {
|
||||
case Some(q) =>
|
||||
Seq(RC.Columns.value.lowerLike(s"%${q.toLowerCase}%"))
|
||||
case None =>
|
||||
Seq.empty
|
||||
}
|
||||
val kindCond = kind match {
|
||||
case Some(k) =>
|
||||
Seq(RC.Columns.kind.is(k))
|
||||
case None =>
|
||||
Seq.empty
|
||||
}
|
||||
|
||||
val q = selectSimple(
|
||||
RC.Columns.all,
|
||||
RC.table,
|
||||
and(
|
||||
Seq(or(RC.Columns.orgId.isIn(orgCond), RC.Columns.personId.isIn(persCond))) ++ queryCond ++ kindCond
|
||||
)
|
||||
) ++ orderBy(RC.Columns.value.f)
|
||||
|
||||
q.query[RContact].stream
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ module Api exposing
|
||||
, deleteUser
|
||||
, getCollective
|
||||
, getCollectiveSettings
|
||||
, getContacts
|
||||
, getEquipments
|
||||
, getInsights
|
||||
, getItemProposals
|
||||
@ -64,6 +65,7 @@ import Api.Model.AuthResult exposing (AuthResult)
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.Collective exposing (Collective)
|
||||
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||
import Api.Model.ContactList exposing (ContactList)
|
||||
import Api.Model.DirectionValue exposing (DirectionValue)
|
||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
||||
@ -98,6 +100,7 @@ import Api.Model.User exposing (User)
|
||||
import Api.Model.UserList exposing (UserList)
|
||||
import Api.Model.UserPass exposing (UserPass)
|
||||
import Api.Model.VersionInfo exposing (VersionInfo)
|
||||
import Data.ContactType exposing (ContactType)
|
||||
import Data.Flags exposing (Flags)
|
||||
import File exposing (File)
|
||||
import Http
|
||||
@ -370,6 +373,48 @@ setCollectiveSettings flags settings receive =
|
||||
}
|
||||
|
||||
|
||||
getContacts :
|
||||
Flags
|
||||
-> Maybe ContactType
|
||||
-> Maybe String
|
||||
-> (Result Http.Error ContactList -> msg)
|
||||
-> Cmd msg
|
||||
getContacts flags kind q receive =
|
||||
let
|
||||
pk =
|
||||
case kind of
|
||||
Just k ->
|
||||
[ "kind=" ++ Data.ContactType.toString k ]
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
pq =
|
||||
case q of
|
||||
Just str ->
|
||||
[ "q=" ++ str ]
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
params =
|
||||
pk ++ pq
|
||||
|
||||
query =
|
||||
case String.join "&" params of
|
||||
"" ->
|
||||
""
|
||||
|
||||
str ->
|
||||
"?" ++ str
|
||||
in
|
||||
Http2.authGet
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/collective/contacts" ++ query
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.ContactList.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- Tags
|
||||
|
||||
|
190
modules/webapp/src/main/elm/Comp/EmailInput.elm
Normal file
190
modules/webapp/src/main/elm/Comp/EmailInput.elm
Normal file
@ -0,0 +1,190 @@
|
||||
module Comp.EmailInput exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api
|
||||
import Api.Model.ContactList exposing (ContactList)
|
||||
import Data.ContactType
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Http
|
||||
import Util.Html exposing (onKeyUp)
|
||||
import Util.List
|
||||
import Util.Maybe
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ input : String
|
||||
, menuOpen : Bool
|
||||
, candidates : List String
|
||||
, active : Maybe String
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ input = ""
|
||||
, menuOpen = False
|
||||
, candidates = []
|
||||
, active = Nothing
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SetInput String
|
||||
| ContactResp (Result Http.Error ContactList)
|
||||
| KeyPress Int
|
||||
| AddEmail String
|
||||
| RemoveEmail String
|
||||
|
||||
|
||||
getCandidates : Flags -> Model -> Cmd Msg
|
||||
getCandidates flags model =
|
||||
case Util.Maybe.fromString model.input of
|
||||
Just q ->
|
||||
Api.getContacts flags (Just Data.ContactType.Email) (Just q) ContactResp
|
||||
|
||||
Nothing ->
|
||||
Cmd.none
|
||||
|
||||
|
||||
update : Flags -> List String -> Msg -> Model -> ( Model, Cmd Msg, List String )
|
||||
update flags current msg model =
|
||||
case msg of
|
||||
SetInput str ->
|
||||
let
|
||||
nm =
|
||||
{ model | input = str, menuOpen = str /= "" }
|
||||
in
|
||||
( nm, getCandidates flags nm, current )
|
||||
|
||||
ContactResp (Ok list) ->
|
||||
( { model
|
||||
| candidates = List.map .value (List.take 10 list.items)
|
||||
, active = Nothing
|
||||
, menuOpen = list.items /= []
|
||||
}
|
||||
, Cmd.none
|
||||
, current
|
||||
)
|
||||
|
||||
ContactResp (Err _) ->
|
||||
( model, Cmd.none, current )
|
||||
|
||||
KeyPress code ->
|
||||
let
|
||||
addCurrent =
|
||||
let
|
||||
email =
|
||||
Maybe.withDefault model.input model.active
|
||||
in
|
||||
update flags current (AddEmail email) model
|
||||
in
|
||||
case Util.Html.intToKeyCode code of
|
||||
Just Util.Html.Up ->
|
||||
let
|
||||
prev =
|
||||
case model.active of
|
||||
Nothing ->
|
||||
List.reverse model.candidates
|
||||
|> List.head
|
||||
|
||||
Just act ->
|
||||
Util.List.findPrev (\e -> e == act) model.candidates
|
||||
in
|
||||
( { model | active = prev }, Cmd.none, current )
|
||||
|
||||
Just Util.Html.Down ->
|
||||
let
|
||||
next =
|
||||
case model.active of
|
||||
Nothing ->
|
||||
List.head model.candidates
|
||||
|
||||
Just act ->
|
||||
Util.List.findNext (\e -> e == act) model.candidates
|
||||
in
|
||||
( { model | active = next }, Cmd.none, current )
|
||||
|
||||
Just Util.Html.Enter ->
|
||||
addCurrent
|
||||
|
||||
Just Util.Html.Space ->
|
||||
addCurrent
|
||||
|
||||
_ ->
|
||||
( model, Cmd.none, current )
|
||||
|
||||
AddEmail str ->
|
||||
( { model | input = "", menuOpen = False }
|
||||
, Cmd.none
|
||||
, Util.List.distinct (current ++ [ String.trim str ])
|
||||
)
|
||||
|
||||
RemoveEmail str ->
|
||||
( model, Cmd.none, List.filter (\e -> e /= str) current )
|
||||
|
||||
|
||||
view : List String -> Model -> Html Msg
|
||||
view values model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "ui search dropdown multiple selection", True )
|
||||
, ( "open", model.menuOpen )
|
||||
]
|
||||
]
|
||||
(List.map renderValue values
|
||||
++ [ input
|
||||
[ type_ "text"
|
||||
, class "search"
|
||||
, placeholder "Recipients…"
|
||||
, onKeyUp KeyPress
|
||||
, onInput SetInput
|
||||
]
|
||||
[ text model.input
|
||||
]
|
||||
]
|
||||
++ [ renderMenu model ]
|
||||
)
|
||||
|
||||
|
||||
renderValue : String -> Html Msg
|
||||
renderValue str =
|
||||
a
|
||||
[ class "ui label"
|
||||
, href "#"
|
||||
, onClick (RemoveEmail str)
|
||||
]
|
||||
[ text str
|
||||
, i [ class "delete icon" ] []
|
||||
]
|
||||
|
||||
|
||||
renderMenu : Model -> Html Msg
|
||||
renderMenu model =
|
||||
let
|
||||
mkItem v =
|
||||
a
|
||||
[ classList
|
||||
[ ( "item", True )
|
||||
, ( "active", model.active == Just v )
|
||||
]
|
||||
, href "#"
|
||||
, onClick (AddEmail v)
|
||||
]
|
||||
[ text v
|
||||
]
|
||||
in
|
||||
div
|
||||
[ classList
|
||||
[ ( "menu", True )
|
||||
, ( "transition visible", model.menuOpen )
|
||||
]
|
||||
]
|
||||
(List.map mkItem model.candidates)
|
@ -715,12 +715,12 @@ update key flags next msg model =
|
||||
|
||||
ItemMailMsg m ->
|
||||
let
|
||||
( im, fa ) =
|
||||
Comp.ItemMail.update m model.itemMail
|
||||
( im, ic, fa ) =
|
||||
Comp.ItemMail.update flags m model.itemMail
|
||||
in
|
||||
case fa of
|
||||
Comp.ItemMail.FormNone ->
|
||||
( { model | itemMail = im }, Cmd.none )
|
||||
( { model | itemMail = im }, Cmd.map ItemMailMsg ic )
|
||||
|
||||
Comp.ItemMail.FormCancel ->
|
||||
( { model
|
||||
@ -728,7 +728,7 @@ update key flags next msg model =
|
||||
, mailOpen = False
|
||||
, mailSendResult = Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
, Cmd.map ItemMailMsg ic
|
||||
)
|
||||
|
||||
Comp.ItemMail.FormSend sm ->
|
||||
@ -739,7 +739,12 @@ update key flags next msg model =
|
||||
, conn = sm.conn
|
||||
}
|
||||
in
|
||||
( model, Api.sendMail flags mail SendMailResp )
|
||||
( model
|
||||
, Cmd.batch
|
||||
[ Cmd.map ItemMailMsg ic
|
||||
, Api.sendMail flags mail SendMailResp
|
||||
]
|
||||
)
|
||||
|
||||
ToggleMail ->
|
||||
( { model | mailOpen = not model.mailOpen }, Cmd.none )
|
||||
|
@ -13,6 +13,7 @@ import Api
|
||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
||||
import Api.Model.SimpleMail exposing (SimpleMail)
|
||||
import Comp.Dropdown
|
||||
import Comp.EmailInput
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
@ -24,7 +25,8 @@ import Util.Http
|
||||
type alias Model =
|
||||
{ connectionModel : Comp.Dropdown.Model String
|
||||
, subject : String
|
||||
, receiver : String
|
||||
, recipients : List String
|
||||
, recipientsModel : Comp.EmailInput.Model
|
||||
, body : String
|
||||
, attachAll : Bool
|
||||
, formError : Maybe String
|
||||
@ -33,7 +35,7 @@ type alias Model =
|
||||
|
||||
type Msg
|
||||
= SetSubject String
|
||||
| SetReceiver String
|
||||
| RecipientMsg Comp.EmailInput.Msg
|
||||
| SetBody String
|
||||
| ConnMsg (Comp.Dropdown.Msg String)
|
||||
| ConnResp (Result Http.Error EmailSettingsList)
|
||||
@ -62,7 +64,8 @@ emptyModel =
|
||||
, placeholder = "Select connection..."
|
||||
}
|
||||
, subject = ""
|
||||
, receiver = ""
|
||||
, recipients = []
|
||||
, recipientsModel = Comp.EmailInput.init
|
||||
, body = ""
|
||||
, attachAll = True
|
||||
, formError = Nothing
|
||||
@ -78,22 +81,29 @@ clear : Model -> Model
|
||||
clear model =
|
||||
{ model
|
||||
| subject = ""
|
||||
, receiver = ""
|
||||
, recipients = []
|
||||
, body = ""
|
||||
}
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, FormAction )
|
||||
update msg model =
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, FormAction )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
SetSubject str ->
|
||||
( { model | subject = str }, FormNone )
|
||||
( { model | subject = str }, Cmd.none, FormNone )
|
||||
|
||||
SetReceiver str ->
|
||||
( { model | receiver = str }, FormNone )
|
||||
RecipientMsg m ->
|
||||
let
|
||||
( em, ec, rec ) =
|
||||
Comp.EmailInput.update flags model.recipients m model.recipientsModel
|
||||
in
|
||||
( { model | recipients = rec, recipientsModel = em }
|
||||
, Cmd.map RecipientMsg ec
|
||||
, FormNone
|
||||
)
|
||||
|
||||
SetBody str ->
|
||||
( { model | body = str }, FormNone )
|
||||
( { model | body = str }, Cmd.none, FormNone )
|
||||
|
||||
ConnMsg m ->
|
||||
let
|
||||
@ -101,10 +111,10 @@ update msg model =
|
||||
--TODO dropdown doesn't use cmd!!
|
||||
Comp.Dropdown.update m model.connectionModel
|
||||
in
|
||||
( { model | connectionModel = cm }, FormNone )
|
||||
( { model | connectionModel = cm }, Cmd.none, FormNone )
|
||||
|
||||
ToggleAttachAll ->
|
||||
( { model | attachAll = not model.attachAll }, FormNone )
|
||||
( { model | attachAll = not model.attachAll }, Cmd.none, FormNone )
|
||||
|
||||
ConnResp (Ok list) ->
|
||||
let
|
||||
@ -128,35 +138,33 @@ update msg model =
|
||||
else
|
||||
Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
, FormNone
|
||||
)
|
||||
|
||||
ConnResp (Err err) ->
|
||||
( { model | formError = Just (Util.Http.errorToString err) }, FormNone )
|
||||
( { model | formError = Just (Util.Http.errorToString err) }, Cmd.none, FormNone )
|
||||
|
||||
Cancel ->
|
||||
( model, FormCancel )
|
||||
( model, Cmd.none, FormCancel )
|
||||
|
||||
Send ->
|
||||
case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
|
||||
( Nothing, conn :: [] ) ->
|
||||
let
|
||||
rec =
|
||||
String.split "," model.receiver
|
||||
|
||||
sm =
|
||||
SimpleMail rec model.subject model.body model.attachAll []
|
||||
SimpleMail model.recipients model.subject model.body model.attachAll []
|
||||
in
|
||||
( model, FormSend { conn = conn, mail = sm } )
|
||||
( model, Cmd.none, FormSend { conn = conn, mail = sm } )
|
||||
|
||||
_ ->
|
||||
( model, FormNone )
|
||||
( model, Cmd.none, FormNone )
|
||||
|
||||
|
||||
isValid : Model -> Bool
|
||||
isValid model =
|
||||
model.receiver
|
||||
/= ""
|
||||
model.recipients
|
||||
/= []
|
||||
&& model.subject
|
||||
/= ""
|
||||
&& model.body
|
||||
@ -182,16 +190,9 @@ view model =
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label []
|
||||
[ text "Receiver(s)"
|
||||
, span [ class "muted" ]
|
||||
[ text "Separate multiple recipients by comma" ]
|
||||
[ text "Recipient(s)"
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, onInput SetReceiver
|
||||
, value model.receiver
|
||||
]
|
||||
[]
|
||||
, Html.map RecipientMsg (Comp.EmailInput.view model.recipients model.recipientsModel)
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label [] [ text "Subject" ]
|
||||
|
@ -18,6 +18,7 @@ type KeyCode
|
||||
| Left
|
||||
| Right
|
||||
| Enter
|
||||
| Space
|
||||
|
||||
|
||||
intToKeyCode : Int -> Maybe KeyCode
|
||||
@ -38,6 +39,9 @@ intToKeyCode code =
|
||||
13 ->
|
||||
Just Enter
|
||||
|
||||
32 ->
|
||||
Just Space
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
@ -29,8 +29,12 @@
|
||||
}
|
||||
|
||||
.default-layout .ui.multiple.search.dropdown>input.search {
|
||||
width: 3.5em;
|
||||
width: auto;
|
||||
}
|
||||
/* .default-layout .ui.multiple.search.dropdown>input.search.long-search { */
|
||||
/* width: auto; */
|
||||
/* } */
|
||||
|
||||
|
||||
.default-layout .job-log {
|
||||
background: #181819;
|
||||
@ -111,6 +115,10 @@ span.small-info {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ui.form textarea.search {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.login-layout, .register-layout, .newinvite-layout {
|
||||
background: #708090;
|
||||
height: 101vh;
|
||||
|
Loading…
Reference in New Issue
Block a user