diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index ae3eabff..388a8d23 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -3,6 +3,8 @@ package docspell.backend.ops import cats.effect._ import cats.implicits._ import cats.data.OptionT +import emil.{MailAddress, SSLType} + import docspell.common._ import docspell.store._ import docspell.store.records.RUserEmail @@ -11,33 +13,71 @@ trait OMail[F[_]] { def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] - def createSettings(data: F[RUserEmail]): F[AddResult] + def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] - def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int] + def createSettings(accId: AccountId, data: OMail.SmtpSettings): F[AddResult] + + def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int] + + def deleteSettings(accId: AccountId, name: Ident): F[Int] } object OMail { + case class SmtpSettings( + name: Ident, + smtpHost: String, + smtpPort: Option[Int], + smtpUser: Option[String], + smtpPassword: Option[Password], + smtpSsl: SSLType, + smtpCertCheck: Boolean, + mailFrom: MailAddress, + mailReplyTo: Option[MailAddress] + ) { + + def toRecord(accId: AccountId) = + RUserEmail.fromAccount( + accId, + name, + smtpHost, + smtpPort, + smtpUser, + smtpPassword, + smtpSsl, + smtpCertCheck, + mailFrom, + mailReplyTo + ) + } + def apply[F[_]: Effect](store: Store[F]): Resource[F, OMail[F]] = Resource.pure(new OMail[F] { def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] = store.transact(RUserEmail.findByAccount(accId, nameQ)) - def createSettings(data: F[RUserEmail]): F[AddResult] = - for { - ru <- data - ins = RUserEmail.insert(ru) - exists = RUserEmail.exists(ru.uid, ru.name) - ar <- store.add(ins, exists) - } yield ar + def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] = + OptionT(store.transact(RUserEmail.getByName(accId, name))) - def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int] = { + def createSettings(accId: AccountId, s: SmtpSettings): F[AddResult] = + (for { + ru <- OptionT(store.transact(s.toRecord(accId).value)) + ins = RUserEmail.insert(ru) + exists = RUserEmail.exists(ru.uid, ru.name) + res <- OptionT.liftF(store.add(ins, exists)) + } yield res).getOrElse(AddResult.Failure(new Exception("User not found"))) + + def updateSettings(accId: AccountId, name: Ident, data: SmtpSettings): F[Int] = { val op = for { um <- OptionT(RUserEmail.getByName(accId, name)) - n <- OptionT.liftF(RUserEmail.update(um.id, data)) + ru <- data.toRecord(accId) + n <- OptionT.liftF(RUserEmail.update(um.id, ru)) } yield n store.transact(op.value).map(_.getOrElse(0)) } + + def deleteSettings(accId: AccountId, name: Ident): F[Int] = + store.transact(RUserEmail.delete(accId, name)) }) } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala index eed7d114..ffb26b49 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala @@ -2,17 +2,21 @@ package docspell.restserver.routes import cats.effect._ import cats.implicits._ +import cats.data.OptionT import org.http4s._ import org.http4s.dsl.Http4sDsl import org.http4s.circe.CirceEntityEncoder._ import org.http4s.circe.CirceEntityDecoder._ +import emil.MailAddress import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OMail import docspell.common._ import docspell.restapi.model._ import docspell.store.records.RUserEmail import docspell.store.EmilUtil +import docspell.restserver.conv.Conversions object MailSettingsRoutes { @@ -29,10 +33,51 @@ object MailSettingsRoutes { resp <- Ok(EmailSettingsList(res.toList)) } yield resp + case GET -> Root / Ident(name) => + (for { + ems <- backend.mail.findSettings(user.account, name) + resp <- OptionT.liftF(Ok(convert(ems))) + } yield resp).getOrElseF(NotFound()) + case req @ POST -> Root => + (for { + in <- OptionT.liftF(req.as[EmailSettings]) + ru = makeSettings(in) + up <- OptionT.liftF(ru.traverse(r => backend.mail.createSettings(user.account, r))) + resp <- OptionT.liftF( + Ok( + up.fold( + err => BasicResult(false, err), + ar => Conversions.basicResult(ar, "Mail settings stored.") + ) + ) + ) + } yield resp).getOrElseF(NotFound()) + + case req @ PUT -> Root / Ident(name) => + (for { + in <- OptionT.liftF(req.as[EmailSettings]) + ru = makeSettings(in) + up <- OptionT.liftF(ru.traverse(r => backend.mail.updateSettings(user.account, name, r))) + resp <- OptionT.liftF( + Ok( + up.fold( + err => BasicResult(false, err), + n => + if (n > 0) BasicResult(true, "Mail settings stored.") + else BasicResult(false, "Mail settings could not be saved") + ) + ) + ) + } yield resp).getOrElseF(NotFound()) + + case DELETE -> Root / Ident(name) => for { - in <- req.as[EmailSettings] - resp <- Ok(BasicResult(false, "not implemented")) + n <- backend.mail.deleteSettings(user.account, name) + resp <- Ok( + if (n > 0) BasicResult(true, "Mail settings removed") + else BasicResult(false, "Mail settings could not be removed") + ) } yield resp } @@ -50,4 +95,29 @@ object MailSettingsRoutes { EmilUtil.sslTypeString(ru.smtpSsl), !ru.smtpCertCheck ) + + def makeSettings(ems: EmailSettings): Either[String, OMail.SmtpSettings] = { + def readMail(str: String): Either[String, MailAddress] = + EmilUtil.readMailAddress(str).left.map(err => s"E-Mail address '$str' invalid: $err") + + def readMailOpt(str: Option[String]): Either[String, Option[MailAddress]] = + str.traverse(readMail) + + for { + from <- readMail(ems.from) + repl <- readMailOpt(ems.replyTo) + sslt <- EmilUtil.readSSLType(ems.sslType) + } yield OMail.SmtpSettings( + ems.name, + ems.smtpHost, + ems.smtpPort, + ems.smtpUser, + ems.smtpPassword, + sslt, + !ems.ignoreCertificates, + from, + repl + ) + + } } diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql index 3595f5a9..fdd711e6 100644 --- a/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql @@ -3,9 +3,9 @@ CREATE TABLE "useremail" ( "uid" varchar(254) not null, "name" varchar(254) not null, "smtp_host" varchar(254) not null, - "smtp_port" int not null, - "smtp_user" varchar(254) not null, - "smtp_password" varchar(254) not null, + "smtp_port" int, + "smtp_user" varchar(254), + "smtp_password" varchar(254), "smtp_ssl" varchar(254) not null, "smtp_certcheck" boolean not null, "mail_from" varchar(254) not null, diff --git a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala index 98cd9dfb..cc67029b 100644 --- a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala +++ b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala @@ -57,7 +57,7 @@ object RUserEmail { now ) - def apply( + def fromAccount( accId: AccountId, name: Ident, smtpHost: String, @@ -130,17 +130,21 @@ object RUserEmail { ).update.run def update(eId: Ident, v: RUserEmail): ConnectionIO[Int] = - updateRow(table, id.is(eId), commas( - name.setTo(v.name), - smtpHost.setTo(v.smtpHost), - smtpPort.setTo(v.smtpPort), - smtpUser.setTo(v.smtpUser), - smtpPass.setTo(v.smtpPassword), - smtpSsl.setTo(v.smtpSsl), - smtpCertCheck.setTo(v.smtpCertCheck), - mailFrom.setTo(v.mailFrom), - mailReplyTo.setTo(v.mailReplyTo) - )).update.run + updateRow( + table, + id.is(eId), + commas( + name.setTo(v.name), + smtpHost.setTo(v.smtpHost), + smtpPort.setTo(v.smtpPort), + smtpUser.setTo(v.smtpUser), + smtpPass.setTo(v.smtpPassword), + smtpSsl.setTo(v.smtpSsl), + smtpCertCheck.setTo(v.smtpCertCheck), + mailFrom.setTo(v.mailFrom), + mailReplyTo.setTo(v.mailReplyTo) + ) + ).update.run def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] = selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector] @@ -162,7 +166,7 @@ object RUserEmail { case None => Seq.empty }) - selectSimple(all, from, and(cond)).query[RUserEmail] + (selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f)).query[RUserEmail] } def findByAccount( @@ -174,6 +178,20 @@ object RUserEmail { def getByName(accId: AccountId, name: Ident): ConnectionIO[Option[RUserEmail]] = findByAccount0(accId, Some(name.id), true).option + def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = { + val uId = RUser.Columns.uid + val uColl = RUser.Columns.cid + val uLogin = RUser.Columns.login + val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) + + deleteFrom( + table, + fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name.is( + connName + ) + ).update.run + } + def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] = getByName(accId, name).map(_.isDefined) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 31bb7eff..dde1217c 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -1,8 +1,10 @@ module Api exposing ( cancelJob , changePassword + , createMailSettings , deleteEquip , deleteItem + , deleteMailSettings , deleteOrg , deletePerson , deleteSource @@ -15,6 +17,7 @@ module Api exposing , getItemProposals , getJobQueueState , getJobQueueStateIn + , getMailSettings , getOrgLight , getOrganizations , getPersons @@ -60,6 +63,8 @@ import Api.Model.BasicResult exposing (BasicResult) import Api.Model.Collective exposing (Collective) import Api.Model.CollectiveSettings exposing (CollectiveSettings) import Api.Model.DirectionValue exposing (DirectionValue) +import Api.Model.EmailSettings exposing (EmailSettings) +import Api.Model.EmailSettingsList exposing (EmailSettingsList) import Api.Model.Equipment exposing (Equipment) import Api.Model.EquipmentList exposing (EquipmentList) import Api.Model.GenInvite exposing (GenInvite) @@ -99,6 +104,57 @@ import Util.File import Util.Http as Http2 + +--- Mail Settings + + +deleteMailSettings : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +deleteMailSettings flags name receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/" ++ name + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +getMailSettings : Flags -> String -> (Result Http.Error EmailSettingsList -> msg) -> Cmd msg +getMailSettings flags query receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings?q=" ++ Url.percentEncode query + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.EmailSettingsList.decoder + } + + +createMailSettings : + Flags + -> Maybe String + -> EmailSettings + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +createMailSettings flags mname ems receive = + case mname of + Just en -> + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/" ++ en + , account = getAccount flags + , body = Http.jsonBody (Api.Model.EmailSettings.encode ems) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + Nothing -> + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.EmailSettings.encode ems) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + + +--- Upload + + upload : Flags -> Maybe String -> ItemUploadMeta -> List File -> (String -> Result Http.Error BasicResult -> msg) -> List (Cmd msg) upload flags sourceId meta files receive = let diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm index 3b83578c..461be387 100644 --- a/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm @@ -4,6 +4,7 @@ module Comp.EmailSettingsForm exposing , emptyModel , getSettings , init + , isValid , update , view ) @@ -40,7 +41,7 @@ emptyModel = { settings = Api.Model.EmailSettings.empty , name = "" , host = "" - , portField = Comp.IntField.init (Just 0) Nothing "SMTP Port" + , portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port" , portNum = Nothing , user = Nothing , passField = Comp.PasswordInput.init @@ -63,7 +64,7 @@ init ems = { settings = ems , name = ems.name , host = ems.smtpHost - , portField = Comp.IntField.init (Just 0) Nothing "SMTP Port" + , portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port" , portNum = ems.smtpPort , user = ems.smtpUser , passField = Comp.PasswordInput.init @@ -184,7 +185,7 @@ view model = ] ] , div [ class "fields" ] - [ div [ class "fifteen wide required field" ] + [ div [ class "thirteen wide required field" ] [ label [] [ text "SMTP Host" ] , input [ type_ "text" @@ -194,7 +195,11 @@ view model = ] [] ] - , Html.map PortMsg (Comp.IntField.view model.portNum model.portField) + , Html.map PortMsg + (Comp.IntField.view model.portNum + "three wide field" + model.portField + ) ] , div [ class "two fields" ] [ div [ class "field" ] diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm index ba2cc6ed..9e3d55b8 100644 --- a/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm @@ -18,6 +18,8 @@ import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) +import Http +import Util.Http type alias Model = @@ -43,9 +45,9 @@ emptyModel = } -init : ( Model, Cmd Msg ) -init = - ( emptyModel, Cmd.none ) +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getMailSettings flags "" MailSettingsResp ) type ViewMode @@ -61,6 +63,10 @@ type Msg | YesNoMsg Comp.YesNoDimmer.Msg | RequestDelete | SetViewMode ViewMode + | Submit + | SubmitResp (Result Http.Error BasicResult) + | LoadSettings + | MailSettingsResp (Result Http.Error EmailSettingsList) update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) @@ -84,8 +90,27 @@ update flags msg model = let ( tm, tc ) = Comp.EmailSettingsTable.update m model.tableModel + + m2 = + { model + | tableModel = tm + , viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table + , formError = + if tm.selected /= Nothing then + Nothing + + else + model.formError + , formModel = + case tm.selected of + Just ems -> + Comp.EmailSettingsForm.init ems + + Nothing -> + model.formModel + } in - ( { model | tableModel = tm }, Cmd.map TableMsg tc ) + ( m2, Cmd.map TableMsg tc ) FormMsg m -> let @@ -95,21 +120,84 @@ update flags msg model = ( { model | formModel = fm }, Cmd.map FormMsg fc ) SetQuery str -> - ( { model | query = str }, Cmd.none ) + let + m = + { model | query = str } + in + ( m, Api.getMailSettings flags str MailSettingsResp ) YesNoMsg m -> let ( dm, flag ) = Comp.YesNoDimmer.update m model.deleteConfirm + + ( mid, _ ) = + Comp.EmailSettingsForm.getSettings model.formModel + + cmd = + case ( flag, mid ) of + ( True, Just name ) -> + Api.deleteMailSettings flags name SubmitResp + + _ -> + Cmd.none in - ( { model | deleteConfirm = dm }, Cmd.none ) + ( { model | deleteConfirm = dm }, cmd ) RequestDelete -> - ( model, Cmd.none ) + update flags (YesNoMsg Comp.YesNoDimmer.activate) model SetViewMode m -> ( { model | viewMode = m }, Cmd.none ) + Submit -> + let + ( mid, ems ) = + Comp.EmailSettingsForm.getSettings model.formModel + + valid = + Comp.EmailSettingsForm.isValid model.formModel + in + if valid then + ( { model | loading = True }, Api.createMailSettings flags mid ems SubmitResp ) + + else + ( { model | formError = Just "Please fill required fields." }, Cmd.none ) + + LoadSettings -> + ( { model | loading = True }, Api.getMailSettings flags model.query MailSettingsResp ) + + SubmitResp (Ok res) -> + if res.success then + let + ( m2, c2 ) = + update flags (SetViewMode Table) model + + ( m3, c3 ) = + update flags LoadSettings m2 + in + ( { m3 | loading = False }, Cmd.batch [ c2, c3 ] ) + + else + ( { model | formError = Just res.message, loading = False }, Cmd.none ) + + SubmitResp (Err err) -> + ( { model | formError = Just (Util.Http.errorToString err), loading = False }, Cmd.none ) + + MailSettingsResp (Ok ems) -> + let + m2 = + { model + | viewMode = Table + , loading = False + , tableModel = Comp.EmailSettingsTable.init ems.items + } + in + ( m2, Cmd.none ) + + MailSettingsResp (Err _) -> + ( { model | loading = False }, Cmd.none ) + view : Model -> Html Msg view model = @@ -171,10 +259,18 @@ viewForm model = [ Maybe.withDefault "" model.formError |> text ] , div [ class "ui divider" ] [] - , button [ class "ui primary button" ] + , button + [ class "ui primary button" + , onClick Submit + , href "#" + ] [ text "Submit" ] - , a [ class "ui secondary button", onClick (SetViewMode Table), href "" ] + , a + [ class "ui secondary button" + , onClick (SetViewMode Table) + , href "" + ] [ text "Cancel" ] , if model.formModel.settings.name /= "" then diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm index d09c9bee..bfa3a388 100644 --- a/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm @@ -2,6 +2,7 @@ module Comp.EmailSettingsTable exposing ( Model , Msg , emptyModel + , init , update , view ) @@ -9,10 +10,12 @@ module Comp.EmailSettingsTable exposing import Api.Model.EmailSettings exposing (EmailSettings) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) type alias Model = { emailSettings : List EmailSettings + , selected : Maybe EmailSettings } @@ -24,6 +27,7 @@ emptyModel = init : List EmailSettings -> Model init ems = { emailSettings = ems + , selected = Nothing } @@ -33,17 +37,40 @@ type Msg update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = - ( model, Cmd.none ) + case msg of + Select ems -> + ( { model | selected = Just ems }, Cmd.none ) view : Model -> Html Msg view model = - table [ class "ui table" ] + table [ class "ui selectable pointer table" ] [ thead [] - [ th [] [ text "Name" ] + [ th [ class "collapsible" ] [ text "Name" ] , th [] [ text "Host/Port" ] , th [] [ text "From" ] ] , tbody [] - [] + (List.map (renderLine model) model.emailSettings) + ] + + +renderLine : Model -> EmailSettings -> Html Msg +renderLine model ems = + let + hostport = + case ems.smtpPort of + Just p -> + ems.smtpHost ++ ":" ++ String.fromInt p + + Nothing -> + ems.smtpHost + in + tr + [ classList [ ( "active", model.selected == Just ems ) ] + , onClick (Select ems) + ] + [ td [ class "collapsible" ] [ text ems.name ] + , td [] [ text hostport ] + , td [] [ text ems.from ] ] diff --git a/modules/webapp/src/main/elm/Comp/IntField.elm b/modules/webapp/src/main/elm/Comp/IntField.elm index 8e519034..498a8ec8 100644 --- a/modules/webapp/src/main/elm/Comp/IntField.elm +++ b/modules/webapp/src/main/elm/Comp/IntField.elm @@ -11,6 +11,7 @@ type alias Model = , label : String , error : Maybe String , lastInput : String + , optional : Bool } @@ -18,26 +19,27 @@ type Msg = SetValue String -init : Maybe Int -> Maybe Int -> String -> Model -init min max label = +init : Maybe Int -> Maybe Int -> Bool -> String -> Model +init min max opt label = { min = min , max = max , label = label , error = Nothing , lastInput = "" + , optional = opt } tooLow : Model -> Int -> Bool tooLow model n = Maybe.map ((<) n) model.min - |> Maybe.withDefault False + |> Maybe.withDefault (not model.optional) tooHigh : Model -> Int -> Bool tooHigh model n = Maybe.map ((>) n) model.max - |> Maybe.withDefault False + |> Maybe.withDefault (not model.optional) update : Msg -> Model -> ( Model, Maybe Int ) @@ -75,16 +77,20 @@ update msg model = ( { m | error = Nothing }, Just n ) Nothing -> - ( { m | error = Just ("'" ++ str ++ "' is not a valid number!") } - , Nothing - ) + if model.optional && String.trim str == "" then + ( { m | error = Nothing }, Nothing ) + + else + ( { m | error = Just ("'" ++ str ++ "' is not a valid number!") } + , Nothing + ) -view : Maybe Int -> Model -> Html Msg -view nval model = +view : Maybe Int -> String -> Model -> Html Msg +view nval classes model = div [ classList - [ ( "field", True ) + [ ( classes, True ) , ( "error", model.error /= Nothing ) ] ] diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 0a17d578..8241f44c 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -13,8 +13,20 @@ update flags msg model = let m = { model | currentTab = Just t } + + ( m2, cmd ) = + case t of + EmailSettingsTab -> + let + ( em, c ) = + Comp.EmailSettingsManage.init flags + in + ( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c ) + + ChangePassTab -> + ( m, Cmd.none ) in - ( m, Cmd.none ) + ( m2, cmd ) ChangePassMsg m -> let diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 24205b67..b96f9d27 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -88,6 +88,10 @@ background-color: #d8dfe5; } +.ui.selectable.pointer.table tr { + cursor: pointer; +} + span.small-info { font-size: smaller; color: rgba(0,0,0,0.6);