mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Starting with mail functionality
This commit is contained in:
@ -2,19 +2,36 @@ module Comp.EmailSettingsForm exposing
|
||||
( Model
|
||||
, Msg
|
||||
, emptyModel
|
||||
, getSettings
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||
import Comp.Dropdown
|
||||
import Comp.IntField
|
||||
import Comp.PasswordInput
|
||||
import Data.SSLType exposing (SSLType)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onInput)
|
||||
import Html.Events exposing (onCheck, onInput)
|
||||
import Util.Maybe
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ settings : EmailSettings
|
||||
, name : String
|
||||
, host : String
|
||||
, portField : Comp.IntField.Model
|
||||
, portNum : Maybe Int
|
||||
, user : Maybe String
|
||||
, passField : Comp.PasswordInput.Model
|
||||
, password : Maybe String
|
||||
, from : String
|
||||
, replyTo : Maybe String
|
||||
, sslType : Comp.Dropdown.Model SSLType
|
||||
, ignoreCertificates : Bool
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +39,22 @@ emptyModel : Model
|
||||
emptyModel =
|
||||
{ settings = Api.Model.EmailSettings.empty
|
||||
, name = ""
|
||||
, host = ""
|
||||
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
|
||||
, portNum = Nothing
|
||||
, user = Nothing
|
||||
, passField = Comp.PasswordInput.init
|
||||
, password = Nothing
|
||||
, from = ""
|
||||
, replyTo = Nothing
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected = Just Data.SSLType.None
|
||||
}
|
||||
, ignoreCertificates = False
|
||||
}
|
||||
|
||||
|
||||
@ -29,21 +62,103 @@ init : EmailSettings -> Model
|
||||
init ems =
|
||||
{ settings = ems
|
||||
, name = ems.name
|
||||
, host = ems.smtpHost
|
||||
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
|
||||
, portNum = ems.smtpPort
|
||||
, user = ems.smtpUser
|
||||
, passField = Comp.PasswordInput.init
|
||||
, password = ems.smtpPassword
|
||||
, from = ems.from
|
||||
, replyTo = ems.replyTo
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected = Data.SSLType.fromString ems.sslType
|
||||
}
|
||||
, ignoreCertificates = ems.ignoreCertificates
|
||||
}
|
||||
|
||||
|
||||
getSettings : Model -> ( Maybe String, EmailSettings )
|
||||
getSettings model =
|
||||
( Util.Maybe.fromString model.settings.name
|
||||
, { name = model.name
|
||||
, smtpHost = model.host
|
||||
, smtpUser = model.user
|
||||
, smtpPort = model.portNum
|
||||
, smtpPassword = model.password
|
||||
, from = model.from
|
||||
, replyTo = model.replyTo
|
||||
, sslType =
|
||||
Comp.Dropdown.getSelected model.sslType
|
||||
|> List.head
|
||||
|> Maybe.withDefault Data.SSLType.None
|
||||
|> Data.SSLType.toString
|
||||
, ignoreCertificates = model.ignoreCertificates
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
type Msg
|
||||
= SetName String
|
||||
| SetHost String
|
||||
| PortMsg Comp.IntField.Msg
|
||||
| SetUser String
|
||||
| PassMsg Comp.PasswordInput.Msg
|
||||
| SSLTypeMsg (Comp.Dropdown.Msg SSLType)
|
||||
| SetFrom String
|
||||
| SetReplyTo String
|
||||
| ToggleCheckCert
|
||||
|
||||
|
||||
isValid : Model -> Bool
|
||||
isValid model =
|
||||
True
|
||||
model.host /= "" && model.name /= ""
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
( model, Cmd.none )
|
||||
case msg of
|
||||
SetName str ->
|
||||
( { model | name = str }, Cmd.none )
|
||||
|
||||
SetHost str ->
|
||||
( { model | host = str }, Cmd.none )
|
||||
|
||||
PortMsg m ->
|
||||
let
|
||||
( pm, val ) =
|
||||
Comp.IntField.update m model.portField
|
||||
in
|
||||
( { model | portField = pm, portNum = val }, Cmd.none )
|
||||
|
||||
SetUser str ->
|
||||
( { model | user = Util.Maybe.fromString str }, Cmd.none )
|
||||
|
||||
PassMsg m ->
|
||||
let
|
||||
( pm, val ) =
|
||||
Comp.PasswordInput.update m model.passField
|
||||
in
|
||||
( { model | passField = pm, password = val }, Cmd.none )
|
||||
|
||||
SSLTypeMsg m ->
|
||||
let
|
||||
( sm, sc ) =
|
||||
Comp.Dropdown.update m model.sslType
|
||||
in
|
||||
( { model | sslType = sm }, Cmd.map SSLTypeMsg sc )
|
||||
|
||||
SetFrom str ->
|
||||
( { model | from = str }, Cmd.none )
|
||||
|
||||
SetReplyTo str ->
|
||||
( { model | replyTo = Util.Maybe.fromString str }, Cmd.none )
|
||||
|
||||
ToggleCheckCert ->
|
||||
( { model | ignoreCertificates = not model.ignoreCertificates }, Cmd.none )
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
@ -61,8 +176,81 @@ view model =
|
||||
[ type_ "text"
|
||||
, value model.name
|
||||
, onInput SetName
|
||||
, placeholder "Connection name"
|
||||
, placeholder "Connection name, e.g. 'gmail.com'"
|
||||
]
|
||||
[]
|
||||
, div [ class "ui info message" ]
|
||||
[ text "The connection name must not contain whitespace or special characters."
|
||||
]
|
||||
]
|
||||
, div [ class "fields" ]
|
||||
[ div [ class "fifteen wide required field" ]
|
||||
[ label [] [ text "SMTP Host" ]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, placeholder "SMTP host name, e.g. 'mail.gmail.com'"
|
||||
, value model.host
|
||||
, onInput SetHost
|
||||
]
|
||||
[]
|
||||
]
|
||||
, Html.map PortMsg (Comp.IntField.view model.portNum model.portField)
|
||||
]
|
||||
, div [ class "two fields" ]
|
||||
[ div [ class "field" ]
|
||||
[ label [] [ text "SMTP User" ]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, placeholder "SMTP Username, e.g. 'your.name@gmail.com'"
|
||||
, Maybe.withDefault "" model.user |> value
|
||||
, onInput SetUser
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label [] [ text "SMTP Password" ]
|
||||
, Html.map PassMsg (Comp.PasswordInput.view model.password model.passField)
|
||||
]
|
||||
]
|
||||
, div [ class "two fields" ]
|
||||
[ div [ class "required field" ]
|
||||
[ label [] [ text "From Address" ]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, placeholder "Sender E-Mail address"
|
||||
, value model.from
|
||||
, onInput SetFrom
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label [] [ text "Reply-To" ]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, placeholder "Optional reply-to E-Mail address"
|
||||
, Maybe.withDefault "" model.replyTo |> value
|
||||
, onInput SetReplyTo
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "two fields" ]
|
||||
[ div [ class "inline field" ]
|
||||
[ div [ class "ui checkbox" ]
|
||||
[ input
|
||||
[ type_ "checkbox"
|
||||
, checked model.ignoreCertificates
|
||||
, onCheck (\_ -> ToggleCheckCert)
|
||||
]
|
||||
[]
|
||||
, label [] [ text "Ignore certificate check" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "two fields" ]
|
||||
[ div [ class "field" ]
|
||||
[ label [] [ text "SSL" ]
|
||||
, Html.map SSLTypeMsg (Comp.Dropdown.view model.sslType)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -17,6 +17,7 @@ import Comp.YesNoDimmer
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
|
||||
|
||||
type alias Model =
|
||||
@ -25,6 +26,7 @@ type alias Model =
|
||||
, viewMode : ViewMode
|
||||
, formError : Maybe String
|
||||
, loading : Bool
|
||||
, query : String
|
||||
, deleteConfirm : Comp.YesNoDimmer.Model
|
||||
}
|
||||
|
||||
@ -36,6 +38,7 @@ emptyModel =
|
||||
, viewMode = Table
|
||||
, formError = Nothing
|
||||
, loading = False
|
||||
, query = ""
|
||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||
}
|
||||
|
||||
@ -53,18 +56,139 @@ type ViewMode
|
||||
type Msg
|
||||
= TableMsg Comp.EmailSettingsTable.Msg
|
||||
| FormMsg Comp.EmailSettingsForm.Msg
|
||||
| SetQuery String
|
||||
| InitNew
|
||||
| YesNoMsg Comp.YesNoDimmer.Msg
|
||||
| RequestDelete
|
||||
| SetViewMode ViewMode
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update flags msg model =
|
||||
( model, Cmd.none )
|
||||
case msg of
|
||||
InitNew ->
|
||||
let
|
||||
ems =
|
||||
Api.Model.EmailSettings.empty
|
||||
|
||||
nm =
|
||||
{ model
|
||||
| viewMode = Form
|
||||
, formError = Nothing
|
||||
, formModel = Comp.EmailSettingsForm.init ems
|
||||
}
|
||||
in
|
||||
( nm, Cmd.none )
|
||||
|
||||
TableMsg m ->
|
||||
let
|
||||
( tm, tc ) =
|
||||
Comp.EmailSettingsTable.update m model.tableModel
|
||||
in
|
||||
( { model | tableModel = tm }, Cmd.map TableMsg tc )
|
||||
|
||||
FormMsg m ->
|
||||
let
|
||||
( fm, fc ) =
|
||||
Comp.EmailSettingsForm.update m model.formModel
|
||||
in
|
||||
( { model | formModel = fm }, Cmd.map FormMsg fc )
|
||||
|
||||
SetQuery str ->
|
||||
( { model | query = str }, Cmd.none )
|
||||
|
||||
YesNoMsg m ->
|
||||
let
|
||||
( dm, flag ) =
|
||||
Comp.YesNoDimmer.update m model.deleteConfirm
|
||||
in
|
||||
( { model | deleteConfirm = dm }, Cmd.none )
|
||||
|
||||
RequestDelete ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SetViewMode m ->
|
||||
( { model | viewMode = m }, Cmd.none )
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model.viewMode of
|
||||
Table ->
|
||||
Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
|
||||
viewTable model
|
||||
|
||||
Form ->
|
||||
Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
|
||||
viewForm model
|
||||
|
||||
|
||||
viewTable : Model -> Html Msg
|
||||
viewTable model =
|
||||
div []
|
||||
[ div [ class "ui secondary menu container" ]
|
||||
[ div [ class "ui container" ]
|
||||
[ div [ class "fitted-item" ]
|
||||
[ div [ class "ui icon input" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, onInput SetQuery
|
||||
, value model.query
|
||||
, placeholder "Search…"
|
||||
]
|
||||
[]
|
||||
, i [ class "ui search icon" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "right menu" ]
|
||||
[ div [ class "fitted-item" ]
|
||||
[ a
|
||||
[ class "ui primary button"
|
||||
, href "#"
|
||||
, onClick InitNew
|
||||
]
|
||||
[ i [ class "plus icon" ] []
|
||||
, text "New Settings"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
|
||||
]
|
||||
|
||||
|
||||
viewForm : Model -> Html Msg
|
||||
viewForm model =
|
||||
div [ class "ui segment" ]
|
||||
[ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
|
||||
, Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui error message", True )
|
||||
, ( "invisible", model.formError == Nothing )
|
||||
]
|
||||
]
|
||||
[ Maybe.withDefault "" model.formError |> text
|
||||
]
|
||||
, div [ class "ui divider" ] []
|
||||
, button [ class "ui primary button" ]
|
||||
[ text "Submit"
|
||||
]
|
||||
, a [ class "ui secondary button", onClick (SetViewMode Table), href "" ]
|
||||
[ text "Cancel"
|
||||
]
|
||||
, if model.formModel.settings.name /= "" then
|
||||
a [ class "ui right floated red button", href "", onClick RequestDelete ]
|
||||
[ text "Delete" ]
|
||||
|
||||
else
|
||||
span [] []
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui dimmer", True )
|
||||
, ( "active", model.loading )
|
||||
]
|
||||
]
|
||||
[ div [ class "ui loader" ] []
|
||||
]
|
||||
]
|
||||
|
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
@ -0,0 +1,108 @@
|
||||
module Comp.IntField exposing (Model, Msg, init, update, view)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onInput)
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ min : Maybe Int
|
||||
, max : Maybe Int
|
||||
, label : String
|
||||
, error : Maybe String
|
||||
, lastInput : String
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SetValue String
|
||||
|
||||
|
||||
init : Maybe Int -> Maybe Int -> String -> Model
|
||||
init min max label =
|
||||
{ min = min
|
||||
, max = max
|
||||
, label = label
|
||||
, error = Nothing
|
||||
, lastInput = ""
|
||||
}
|
||||
|
||||
|
||||
tooLow : Model -> Int -> Bool
|
||||
tooLow model n =
|
||||
Maybe.map ((<) n) model.min
|
||||
|> Maybe.withDefault False
|
||||
|
||||
|
||||
tooHigh : Model -> Int -> Bool
|
||||
tooHigh model n =
|
||||
Maybe.map ((>) n) model.max
|
||||
|> Maybe.withDefault False
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Maybe Int )
|
||||
update msg model =
|
||||
let
|
||||
tooHighError =
|
||||
Maybe.withDefault 0 model.max
|
||||
|> String.fromInt
|
||||
|> (++) "Number must be <= "
|
||||
|
||||
tooLowError =
|
||||
Maybe.withDefault 0 model.min
|
||||
|> String.fromInt
|
||||
|> (++) "Number must be >= "
|
||||
in
|
||||
case msg of
|
||||
SetValue str ->
|
||||
let
|
||||
m =
|
||||
{ model | lastInput = str }
|
||||
in
|
||||
case String.toInt str of
|
||||
Just n ->
|
||||
if tooLow model n then
|
||||
( { m | error = Just tooLowError }
|
||||
, Nothing
|
||||
)
|
||||
|
||||
else if tooHigh model n then
|
||||
( { m | error = Just tooHighError }
|
||||
, Nothing
|
||||
)
|
||||
|
||||
else
|
||||
( { m | error = Nothing }, Just n )
|
||||
|
||||
Nothing ->
|
||||
( { m | error = Just ("'" ++ str ++ "' is not a valid number!") }
|
||||
, Nothing
|
||||
)
|
||||
|
||||
|
||||
view : Maybe Int -> Model -> Html Msg
|
||||
view nval model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "field", True )
|
||||
, ( "error", model.error /= Nothing )
|
||||
]
|
||||
]
|
||||
[ label [] [ text model.label ]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, Maybe.map String.fromInt nval
|
||||
|> Maybe.withDefault model.lastInput
|
||||
|> value
|
||||
, onInput SetValue
|
||||
]
|
||||
[]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui pointing red basic label", True )
|
||||
, ( "hidden", model.error == Nothing )
|
||||
]
|
||||
]
|
||||
[ Maybe.withDefault "" model.error |> text
|
||||
]
|
||||
]
|
74
modules/webapp/src/main/elm/Comp/PasswordInput.elm
Normal file
74
modules/webapp/src/main/elm/Comp/PasswordInput.elm
Normal file
@ -0,0 +1,74 @@
|
||||
module Comp.PasswordInput exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Util.Maybe
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ show : Bool
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ show = False
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= ToggleShow (Maybe String)
|
||||
| SetPassword String
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Maybe String )
|
||||
update msg model =
|
||||
case msg of
|
||||
ToggleShow pw ->
|
||||
( { model | show = not model.show }
|
||||
, pw
|
||||
)
|
||||
|
||||
SetPassword str ->
|
||||
let
|
||||
pw =
|
||||
Util.Maybe.fromString str
|
||||
in
|
||||
( model, pw )
|
||||
|
||||
|
||||
view : Maybe String -> Model -> Html Msg
|
||||
view pw model =
|
||||
div [ class "ui left action input" ]
|
||||
[ button
|
||||
[ class "ui icon button"
|
||||
, type_ "button"
|
||||
, onClick (ToggleShow pw)
|
||||
]
|
||||
[ i
|
||||
[ classList
|
||||
[ ( "ui eye icon", True )
|
||||
, ( "slash", model.show )
|
||||
]
|
||||
]
|
||||
[]
|
||||
]
|
||||
, input
|
||||
[ type_ <|
|
||||
if model.show then
|
||||
"text"
|
||||
|
||||
else
|
||||
"password"
|
||||
, onInput SetPassword
|
||||
, Maybe.withDefault "" pw |> value
|
||||
]
|
||||
[]
|
||||
]
|
60
modules/webapp/src/main/elm/Data/SSLType.elm
Normal file
60
modules/webapp/src/main/elm/Data/SSLType.elm
Normal file
@ -0,0 +1,60 @@
|
||||
module Data.SSLType exposing
|
||||
( SSLType(..)
|
||||
, all
|
||||
, fromString
|
||||
, label
|
||||
, toString
|
||||
)
|
||||
|
||||
|
||||
type SSLType
|
||||
= None
|
||||
| SSL
|
||||
| StartTLS
|
||||
|
||||
|
||||
all : List SSLType
|
||||
all =
|
||||
[ None, SSL, StartTLS ]
|
||||
|
||||
|
||||
toString : SSLType -> String
|
||||
toString st =
|
||||
case st of
|
||||
None ->
|
||||
"none"
|
||||
|
||||
SSL ->
|
||||
"ssl"
|
||||
|
||||
StartTLS ->
|
||||
"starttls"
|
||||
|
||||
|
||||
fromString : String -> Maybe SSLType
|
||||
fromString str =
|
||||
case String.toLower str of
|
||||
"none" ->
|
||||
Just None
|
||||
|
||||
"ssl" ->
|
||||
Just SSL
|
||||
|
||||
"starttls" ->
|
||||
Just StartTLS
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
||||
label : SSLType -> String
|
||||
label st =
|
||||
case st of
|
||||
None ->
|
||||
"None"
|
||||
|
||||
SSL ->
|
||||
"SSL/TLS"
|
||||
|
||||
StartTLS ->
|
||||
"StartTLS"
|
@ -1,5 +1,6 @@
|
||||
module Util.Maybe exposing
|
||||
( isEmpty
|
||||
( fromString
|
||||
, isEmpty
|
||||
, nonEmpty
|
||||
, or
|
||||
, withDefault
|
||||
@ -38,3 +39,16 @@ or listma =
|
||||
|
||||
Nothing ->
|
||||
or els
|
||||
|
||||
|
||||
fromString : String -> Maybe String
|
||||
fromString str =
|
||||
let
|
||||
s =
|
||||
String.trim str
|
||||
in
|
||||
if s == "" then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just str
|
||||
|
Reference in New Issue
Block a user