mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-05 22:55:58 +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
|
package docspell.backend.ops
|
||||||
|
|
||||||
|
import fs2.Stream
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import cats.effect.{Effect, Resource}
|
import cats.effect.{Effect, Resource}
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.{AddResult, Store}
|
import docspell.store.{AddResult, Store}
|
||||||
import docspell.store.records.{RCollective, RUser}
|
import docspell.store.records.{RCollective, RContact, RUser}
|
||||||
import OCollective._
|
import OCollective._
|
||||||
import docspell.backend.PasswordCrypt
|
import docspell.backend.PasswordCrypt
|
||||||
import docspell.store.queries.QCollective
|
import docspell.store.queries.QCollective
|
||||||
@ -30,6 +31,13 @@ trait OCollective[F[_]] {
|
|||||||
current: Password,
|
current: Password,
|
||||||
newPass: Password
|
newPass: Password
|
||||||
): F[PassChangeResult]
|
): F[PassChangeResult]
|
||||||
|
|
||||||
|
def getContacts(
|
||||||
|
collective: Ident,
|
||||||
|
query: Option[String],
|
||||||
|
kind: Option[ContactKind]
|
||||||
|
): Stream[F, RContact]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object OCollective {
|
object OCollective {
|
||||||
@ -119,5 +127,13 @@ object OCollective {
|
|||||||
|
|
||||||
store.transact(q)
|
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">
|
<img src="../img/mail-item-1.jpg">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Then write the mail. Multiple recipients may be specified by
|
Then write the mail. Multiple recipients may be specified. The input
|
||||||
separating their addresses by comma.
|
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
|
If you have multiple mail settings defined, you can choose in the top
|
||||||
dropdown which account to use for sending.
|
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
|
The last checkbox allows to choose whether docspell should add all
|
||||||
attachments of the item to the mail. If it is unchecked, no
|
attachments of the item to the mail. If it is unchecked, no
|
||||||
attachments will be added. It is currently not possible to pick
|
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 *Cancel* will delete the inputs and close the mail form, but
|
||||||
clicking the envelope icon again, will only close the form without
|
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:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ItemInsights"
|
$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:
|
/sec/user:
|
||||||
get:
|
get:
|
||||||
tags: [ Collective ]
|
tags: [ Collective ]
|
||||||
@ -2182,6 +2201,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
country:
|
country:
|
||||||
type: string
|
type: string
|
||||||
|
ContactList:
|
||||||
|
description: |
|
||||||
|
A list of contacts.
|
||||||
|
required:
|
||||||
|
- items
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Contact"
|
||||||
Contact:
|
Contact:
|
||||||
description: |
|
description: |
|
||||||
Contact information.
|
Contact information.
|
||||||
@ -2487,3 +2516,11 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
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.backend.auth.AuthToken
|
||||||
import docspell.restapi.model._
|
import docspell.restapi.model._
|
||||||
import docspell.restserver.conv.Conversions
|
import docspell.restserver.conv.Conversions
|
||||||
import docspell.restserver.http4s.ResponseGenerator
|
import docspell.restserver.http4s._
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import org.http4s.circe.CirceEntityDecoder._
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
@ -39,6 +39,16 @@ object CollectiveRoutes {
|
|||||||
resp <- sett.toResponse()
|
resp <- sett.toResponse()
|
||||||
} yield resp
|
} 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 =>
|
case GET -> Root =>
|
||||||
for {
|
for {
|
||||||
collDb <- backend.collective.find(user.account.collective)
|
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 =
|
def isIn(values: Seq[Fragment]): Fragment =
|
||||||
f ++ fr"IN (" ++ commas(values) ++ fr")"
|
f ++ fr"IN (" ++ commas(values) ++ fr")"
|
||||||
|
|
||||||
|
def isIn(frag: Fragment): Fragment =
|
||||||
|
f ++ fr"IN (" ++ frag ++ fr")"
|
||||||
|
|
||||||
def isOrDiscard[A: Put](value: Option[A]): Fragment =
|
def isOrDiscard[A: Put](value: Option[A]): Fragment =
|
||||||
value match {
|
value match {
|
||||||
case Some(v) => is(v)
|
case Some(v) => is(v)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package docspell.store.queries
|
package docspell.store.queries
|
||||||
|
|
||||||
|
import fs2.Stream
|
||||||
import doobie._
|
import doobie._
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
import docspell.common.{Direction, Ident}
|
import docspell.common.{Direction, Ident}
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
import docspell.store.records.{RAttachment, RItem, RTag, RTagItem}
|
import docspell.store.records._
|
||||||
|
import docspell.common.ContactKind
|
||||||
|
|
||||||
object QCollective {
|
object QCollective {
|
||||||
|
|
||||||
@ -50,4 +52,38 @@ object QCollective {
|
|||||||
} yield InsightData(n0, n1, n2.getOrElse(0), Map.from(n3))
|
} 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
|
, deleteUser
|
||||||
, getCollective
|
, getCollective
|
||||||
, getCollectiveSettings
|
, getCollectiveSettings
|
||||||
|
, getContacts
|
||||||
, getEquipments
|
, getEquipments
|
||||||
, getInsights
|
, getInsights
|
||||||
, getItemProposals
|
, getItemProposals
|
||||||
@ -64,6 +65,7 @@ import Api.Model.AuthResult exposing (AuthResult)
|
|||||||
import Api.Model.BasicResult exposing (BasicResult)
|
import Api.Model.BasicResult exposing (BasicResult)
|
||||||
import Api.Model.Collective exposing (Collective)
|
import Api.Model.Collective exposing (Collective)
|
||||||
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||||
|
import Api.Model.ContactList exposing (ContactList)
|
||||||
import Api.Model.DirectionValue exposing (DirectionValue)
|
import Api.Model.DirectionValue exposing (DirectionValue)
|
||||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
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.UserList exposing (UserList)
|
||||||
import Api.Model.UserPass exposing (UserPass)
|
import Api.Model.UserPass exposing (UserPass)
|
||||||
import Api.Model.VersionInfo exposing (VersionInfo)
|
import Api.Model.VersionInfo exposing (VersionInfo)
|
||||||
|
import Data.ContactType exposing (ContactType)
|
||||||
import Data.Flags exposing (Flags)
|
import Data.Flags exposing (Flags)
|
||||||
import File exposing (File)
|
import File exposing (File)
|
||||||
import Http
|
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
|
-- 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 ->
|
ItemMailMsg m ->
|
||||||
let
|
let
|
||||||
( im, fa ) =
|
( im, ic, fa ) =
|
||||||
Comp.ItemMail.update m model.itemMail
|
Comp.ItemMail.update flags m model.itemMail
|
||||||
in
|
in
|
||||||
case fa of
|
case fa of
|
||||||
Comp.ItemMail.FormNone ->
|
Comp.ItemMail.FormNone ->
|
||||||
( { model | itemMail = im }, Cmd.none )
|
( { model | itemMail = im }, Cmd.map ItemMailMsg ic )
|
||||||
|
|
||||||
Comp.ItemMail.FormCancel ->
|
Comp.ItemMail.FormCancel ->
|
||||||
( { model
|
( { model
|
||||||
@ -728,7 +728,7 @@ update key flags next msg model =
|
|||||||
, mailOpen = False
|
, mailOpen = False
|
||||||
, mailSendResult = Nothing
|
, mailSendResult = Nothing
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.map ItemMailMsg ic
|
||||||
)
|
)
|
||||||
|
|
||||||
Comp.ItemMail.FormSend sm ->
|
Comp.ItemMail.FormSend sm ->
|
||||||
@ -739,7 +739,12 @@ update key flags next msg model =
|
|||||||
, conn = sm.conn
|
, conn = sm.conn
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
( model, Api.sendMail flags mail SendMailResp )
|
( model
|
||||||
|
, Cmd.batch
|
||||||
|
[ Cmd.map ItemMailMsg ic
|
||||||
|
, Api.sendMail flags mail SendMailResp
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
ToggleMail ->
|
ToggleMail ->
|
||||||
( { model | mailOpen = not model.mailOpen }, Cmd.none )
|
( { model | mailOpen = not model.mailOpen }, Cmd.none )
|
||||||
|
@ -13,6 +13,7 @@ import Api
|
|||||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
||||||
import Api.Model.SimpleMail exposing (SimpleMail)
|
import Api.Model.SimpleMail exposing (SimpleMail)
|
||||||
import Comp.Dropdown
|
import Comp.Dropdown
|
||||||
|
import Comp.EmailInput
|
||||||
import Data.Flags exposing (Flags)
|
import Data.Flags exposing (Flags)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
@ -24,7 +25,8 @@ import Util.Http
|
|||||||
type alias Model =
|
type alias Model =
|
||||||
{ connectionModel : Comp.Dropdown.Model String
|
{ connectionModel : Comp.Dropdown.Model String
|
||||||
, subject : String
|
, subject : String
|
||||||
, receiver : String
|
, recipients : List String
|
||||||
|
, recipientsModel : Comp.EmailInput.Model
|
||||||
, body : String
|
, body : String
|
||||||
, attachAll : Bool
|
, attachAll : Bool
|
||||||
, formError : Maybe String
|
, formError : Maybe String
|
||||||
@ -33,7 +35,7 @@ type alias Model =
|
|||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= SetSubject String
|
= SetSubject String
|
||||||
| SetReceiver String
|
| RecipientMsg Comp.EmailInput.Msg
|
||||||
| SetBody String
|
| SetBody String
|
||||||
| ConnMsg (Comp.Dropdown.Msg String)
|
| ConnMsg (Comp.Dropdown.Msg String)
|
||||||
| ConnResp (Result Http.Error EmailSettingsList)
|
| ConnResp (Result Http.Error EmailSettingsList)
|
||||||
@ -62,7 +64,8 @@ emptyModel =
|
|||||||
, placeholder = "Select connection..."
|
, placeholder = "Select connection..."
|
||||||
}
|
}
|
||||||
, subject = ""
|
, subject = ""
|
||||||
, receiver = ""
|
, recipients = []
|
||||||
|
, recipientsModel = Comp.EmailInput.init
|
||||||
, body = ""
|
, body = ""
|
||||||
, attachAll = True
|
, attachAll = True
|
||||||
, formError = Nothing
|
, formError = Nothing
|
||||||
@ -78,22 +81,29 @@ clear : Model -> Model
|
|||||||
clear model =
|
clear model =
|
||||||
{ model
|
{ model
|
||||||
| subject = ""
|
| subject = ""
|
||||||
, receiver = ""
|
, recipients = []
|
||||||
, body = ""
|
, body = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, FormAction )
|
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, FormAction )
|
||||||
update msg model =
|
update flags msg model =
|
||||||
case msg of
|
case msg of
|
||||||
SetSubject str ->
|
SetSubject str ->
|
||||||
( { model | subject = str }, FormNone )
|
( { model | subject = str }, Cmd.none, FormNone )
|
||||||
|
|
||||||
SetReceiver str ->
|
RecipientMsg m ->
|
||||||
( { model | receiver = str }, FormNone )
|
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 ->
|
SetBody str ->
|
||||||
( { model | body = str }, FormNone )
|
( { model | body = str }, Cmd.none, FormNone )
|
||||||
|
|
||||||
ConnMsg m ->
|
ConnMsg m ->
|
||||||
let
|
let
|
||||||
@ -101,10 +111,10 @@ update msg model =
|
|||||||
--TODO dropdown doesn't use cmd!!
|
--TODO dropdown doesn't use cmd!!
|
||||||
Comp.Dropdown.update m model.connectionModel
|
Comp.Dropdown.update m model.connectionModel
|
||||||
in
|
in
|
||||||
( { model | connectionModel = cm }, FormNone )
|
( { model | connectionModel = cm }, Cmd.none, FormNone )
|
||||||
|
|
||||||
ToggleAttachAll ->
|
ToggleAttachAll ->
|
||||||
( { model | attachAll = not model.attachAll }, FormNone )
|
( { model | attachAll = not model.attachAll }, Cmd.none, FormNone )
|
||||||
|
|
||||||
ConnResp (Ok list) ->
|
ConnResp (Ok list) ->
|
||||||
let
|
let
|
||||||
@ -128,35 +138,33 @@ update msg model =
|
|||||||
else
|
else
|
||||||
Nothing
|
Nothing
|
||||||
}
|
}
|
||||||
|
, Cmd.none
|
||||||
, FormNone
|
, FormNone
|
||||||
)
|
)
|
||||||
|
|
||||||
ConnResp (Err err) ->
|
ConnResp (Err err) ->
|
||||||
( { model | formError = Just (Util.Http.errorToString err) }, FormNone )
|
( { model | formError = Just (Util.Http.errorToString err) }, Cmd.none, FormNone )
|
||||||
|
|
||||||
Cancel ->
|
Cancel ->
|
||||||
( model, FormCancel )
|
( model, Cmd.none, FormCancel )
|
||||||
|
|
||||||
Send ->
|
Send ->
|
||||||
case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
|
case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
|
||||||
( Nothing, conn :: [] ) ->
|
( Nothing, conn :: [] ) ->
|
||||||
let
|
let
|
||||||
rec =
|
|
||||||
String.split "," model.receiver
|
|
||||||
|
|
||||||
sm =
|
sm =
|
||||||
SimpleMail rec model.subject model.body model.attachAll []
|
SimpleMail model.recipients model.subject model.body model.attachAll []
|
||||||
in
|
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 -> Bool
|
||||||
isValid model =
|
isValid model =
|
||||||
model.receiver
|
model.recipients
|
||||||
/= ""
|
/= []
|
||||||
&& model.subject
|
&& model.subject
|
||||||
/= ""
|
/= ""
|
||||||
&& model.body
|
&& model.body
|
||||||
@ -182,16 +190,9 @@ view model =
|
|||||||
]
|
]
|
||||||
, div [ class "field" ]
|
, div [ class "field" ]
|
||||||
[ label []
|
[ label []
|
||||||
[ text "Receiver(s)"
|
[ text "Recipient(s)"
|
||||||
, span [ class "muted" ]
|
|
||||||
[ text "Separate multiple recipients by comma" ]
|
|
||||||
]
|
]
|
||||||
, input
|
, Html.map RecipientMsg (Comp.EmailInput.view model.recipients model.recipientsModel)
|
||||||
[ type_ "text"
|
|
||||||
, onInput SetReceiver
|
|
||||||
, value model.receiver
|
|
||||||
]
|
|
||||||
[]
|
|
||||||
]
|
]
|
||||||
, div [ class "field" ]
|
, div [ class "field" ]
|
||||||
[ label [] [ text "Subject" ]
|
[ label [] [ text "Subject" ]
|
||||||
|
@ -18,6 +18,7 @@ type KeyCode
|
|||||||
| Left
|
| Left
|
||||||
| Right
|
| Right
|
||||||
| Enter
|
| Enter
|
||||||
|
| Space
|
||||||
|
|
||||||
|
|
||||||
intToKeyCode : Int -> Maybe KeyCode
|
intToKeyCode : Int -> Maybe KeyCode
|
||||||
@ -38,6 +39,9 @@ intToKeyCode code =
|
|||||||
13 ->
|
13 ->
|
||||||
Just Enter
|
Just Enter
|
||||||
|
|
||||||
|
32 ->
|
||||||
|
Just Space
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
|
@ -29,8 +29,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.default-layout .ui.multiple.search.dropdown>input.search {
|
.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 {
|
.default-layout .job-log {
|
||||||
background: #181819;
|
background: #181819;
|
||||||
@ -111,6 +115,10 @@ span.small-info {
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.form textarea.search {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.login-layout, .register-layout, .newinvite-layout {
|
.login-layout, .register-layout, .newinvite-layout {
|
||||||
background: #708090;
|
background: #708090;
|
||||||
height: 101vh;
|
height: 101vh;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user