From 3a90d874a5721dac463a0f96411f0339cac6acb1 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 20 Apr 2020 22:53:08 +0200 Subject: [PATCH] Improve form --- .../scala/docspell/backend/ops/OTag.scala | 8 + .../src/main/resources/docspell-openapi.yml | 22 +-- .../routes/NotifyDueItemsRoutes.scala | 34 +++-- .../scala/docspell/store/records/RTag.scala | 5 + modules/webapp/src/main/elm/Api.elm | 18 +++ .../src/main/elm/Comp/CalEventInput.elm | 144 +++++++----------- .../src/main/elm/Comp/NotificationForm.elm | 69 +++++++-- modules/webapp/src/main/elm/Data/CalEvent.elm | 107 +++++++++++++ .../webapp/src/main/elm/Data/Validated.elm | 20 +++ modules/webapp/src/main/webjar/docspell.css | 2 + 10 files changed, 297 insertions(+), 132 deletions(-) create mode 100644 modules/webapp/src/main/elm/Data/CalEvent.elm create mode 100644 modules/webapp/src/main/elm/Data/Validated.elm diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala index 79824a2a..45eaa230 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala @@ -15,6 +15,10 @@ trait OTag[F[_]] { def update(s: RTag): F[AddResult] def delete(id: Ident, collective: Ident): F[AddResult] + + /** Load all tags given their ids. Ids that are not available are ignored. + */ + def loadAll(ids: List[Ident]): F[Vector[RTag]] } object OTag { @@ -48,5 +52,9 @@ object OTag { } yield n0.getOrElse(0) + n1.getOrElse(0) store.transact(io).attempt.map(AddResult.fromUpdate) } + + def loadAll(ids: List[Ident]): F[Vector[RTag]] = + if (ids.isEmpty) Vector.empty.pure[F] + else store.transact(RTag.findAllById(ids)) }) } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 5d54b26e..8068b4bd 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1597,7 +1597,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/NotificationData" + $ref: "#/components/schemas/NotificationSettings" post: tags: [ Notification ] summary: Change current settings for "Notify Due Items" task @@ -1683,27 +1683,11 @@ components: tagsInclude: type: array items: - type: string - format: ident + $ref: "#/components/schemas/Tag" tagsExclude: type: array items: - type: string - format: ident - NotificationData: - description: | - Data for the notification settings. - required: - - settings - properties: - settings: - $ref: "#/components/schemas/NotificationSettings" - nextRun: - type: integer - format: date-time - lastRun: - type: integer - format: date-time + $ref: "#/components/schemas/Tag" SentMails: description: | A list of sent mails. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala index 19482688..76ac9e20 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala @@ -25,7 +25,8 @@ object NotifyDueItemsRoutes { case GET -> Root => for { task <- ut.getNotifyDueItems(user.account) - resp <- Ok(convert(task)) + res <- taskToSettings(user.account, backend, task) + resp <- Ok(res) } yield resp case req @ POST -> Root => @@ -41,9 +42,6 @@ object NotifyDueItemsRoutes { } } - def convert(task: UserTask[NotifyDueItemsArgs]): NotificationData = - NotificationData(taskToSettings(task), None, None) - def makeTask( user: AccountId, settings: NotificationSettings @@ -58,20 +56,34 @@ object NotifyDueItemsRoutes { settings.smtpConnection, settings.recipients, settings.remindDays, - settings.tagsInclude.map(Ident.unsafe), - settings.tagsExclude.map(Ident.unsafe) + settings.tagsInclude.map(_.id), + settings.tagsExclude.map(_.id) ) ) - def taskToSettings(task: UserTask[NotifyDueItemsArgs]): NotificationSettings = - NotificationSettings( + // TODO this should be inside the backend code and not here + def taskToSettings[F[_]: Sync]( + account: AccountId, + backend: BackendApp[F], + task: UserTask[NotifyDueItemsArgs] + ): F[NotificationSettings] = + for { + tinc <- backend.tag.loadAll(task.args.tagsInclude) + texc <- backend.tag.loadAll(task.args.tagsExclude) + conn <- backend.mail + .getSettings(account, None) + .map( + _.find(_.name == task.args.smtpConnection) + .map(_.name) + ) + } yield NotificationSettings( task.id, task.enabled, - task.args.smtpConnection, + conn.getOrElse(Ident.unsafe("none")), task.args.recipients, task.timer, task.args.remindDays, - task.args.tagsInclude.map(_.id), - task.args.tagsExclude.map(_.id) + tinc.map(Conversions.mkTag).toList, + texc.map(Conversions.mkTag).toList ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RTag.scala b/modules/store/src/main/scala/docspell/store/records/RTag.scala index 39cd8b35..29ce9d96 100644 --- a/modules/store/src/main/scala/docspell/store/records/RTag.scala +++ b/modules/store/src/main/scala/docspell/store/records/RTag.scala @@ -82,6 +82,11 @@ object RTag { sql.query[RTag].to[Vector] } + def findAllById(ids: List[Ident]): ConnectionIO[Vector[RTag]] = + selectSimple(all, table, tid.isIn(ids.map(id => sql"$id").toSeq)) + .query[RTag] + .to[Vector] + def findByItem(itemId: Ident): ConnectionIO[Vector[RTag]] = { val rcol = all.map(_.prefix("t")) (selectSimple( diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index b373c8a2..988c6945 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -21,6 +21,7 @@ module Api exposing , getJobQueueState , getJobQueueStateIn , getMailSettings + , getNotifyDueItems , getOrgLight , getOrganizations , getPersons @@ -85,6 +86,7 @@ import Api.Model.ItemProposals exposing (ItemProposals) import Api.Model.ItemSearch exposing (ItemSearch) import Api.Model.ItemUploadMeta exposing (ItemUploadMeta) import Api.Model.JobQueueState exposing (JobQueueState) +import Api.Model.NotificationSettings exposing (NotificationSettings) import Api.Model.OptionalDate exposing (OptionalDate) import Api.Model.OptionalId exposing (OptionalId) import Api.Model.OptionalText exposing (OptionalText) @@ -117,6 +119,22 @@ import Util.Http as Http2 +--- NotifyDueItems + + +getNotifyDueItems : + Flags + -> (Result Http.Error NotificationSettings -> msg) + -> Cmd msg +getNotifyDueItems flags receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.NotificationSettings.decoder + } + + + --- CalEvent diff --git a/modules/webapp/src/main/elm/Comp/CalEventInput.elm b/modules/webapp/src/main/elm/Comp/CalEventInput.elm index 0b455669..6565d230 100644 --- a/modules/webapp/src/main/elm/Comp/CalEventInput.elm +++ b/modules/webapp/src/main/elm/Comp/CalEventInput.elm @@ -2,7 +2,6 @@ module Comp.CalEventInput exposing ( Model , Msg , init - , initialSchedule , update , view ) @@ -10,7 +9,9 @@ module Comp.CalEventInput exposing import Api import Api.Model.CalEventCheck exposing (CalEventCheck) import Api.Model.CalEventCheckResult exposing (CalEventCheckResult) +import Data.CalEvent exposing (CalEvent) import Data.Flags exposing (Flags) +import Data.Validated exposing (Validated(..)) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onInput) @@ -21,14 +22,7 @@ import Util.Time type alias Model = - { year : String - , month : String - , day : String - , hour : String - , minute : String - , weekday : Maybe String - , event : Maybe String - , checkResult : Maybe CalEventCheckResult + { checkResult : Maybe CalEventCheckResult } @@ -39,68 +33,29 @@ type Msg | SetHour String | SetMinute String | SetWeekday String - | CheckInputMsg (Result Http.Error CalEventCheckResult) + | CheckInputMsg CalEvent (Result Http.Error CalEventCheckResult) -initialSchedule : String -initialSchedule = - "*-*-01 00:00" +init : Flags -> CalEvent -> ( Model, Cmd Msg ) +init flags ev = + ( Model Nothing, checkInput flags ev ) -init : Flags -> ( Model, Cmd Msg ) -init flags = +checkInput : Flags -> CalEvent -> Cmd Msg +checkInput flags ev = let - model = - { year = "*" - , month = "*" - , day = "1" - , hour = "0" - , minute = "0" - , weekday = Nothing - , event = Nothing - , checkResult = Nothing - } - in - ( model, checkInput flags model ) - - -toEvent : Model -> String -toEvent model = - let - datetime = - model.year - ++ "-" - ++ model.month - ++ "-" - ++ model.day - ++ " " - ++ model.hour - ++ ":" - ++ model.minute - in - case model.weekday of - Just wd -> - wd ++ " " ++ datetime - - Nothing -> - datetime - - -checkInput : Flags -> Model -> Cmd Msg -checkInput flags model = - let - event = - toEvent model + eventStr = + Data.CalEvent.makeEvent ev input = - CalEventCheck event + CalEventCheck eventStr in - Api.checkCalEvent flags input CheckInputMsg + Api.checkCalEvent flags input (CheckInputMsg ev) -withCheckInput : Flags -> Model -> ( Model, Cmd Msg, Maybe String ) -withCheckInput flags model = - ( model, checkInput flags model, Nothing ) +withCheckInput : Flags -> CalEvent -> Model -> ( Model, Cmd Msg, Validated CalEvent ) +withCheckInput flags ev model = + ( model, checkInput flags ev, Unknown ev ) isCheckError : Model -> Bool @@ -110,46 +65,49 @@ isCheckError model = |> not -update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe String ) -update flags msg model = +update : Flags -> CalEvent -> Msg -> Model -> ( Model, Cmd Msg, Validated CalEvent ) +update flags ev msg model = case msg of SetYear str -> - withCheckInput flags { model | year = str } + withCheckInput flags { ev | year = str } model SetMonth str -> - withCheckInput flags { model | month = str } + withCheckInput flags { ev | month = str } model SetDay str -> - withCheckInput flags { model | day = str } + withCheckInput flags { ev | day = str } model SetHour str -> - withCheckInput flags { model | hour = str } + withCheckInput flags { ev | hour = str } model SetMinute str -> - withCheckInput flags { model | minute = str } + withCheckInput flags { ev | minute = str } model SetWeekday str -> - withCheckInput flags { model | weekday = Util.Maybe.fromString str } + withCheckInput flags { ev | weekday = Util.Maybe.fromString str } model - CheckInputMsg (Ok res) -> + CheckInputMsg event (Ok res) -> let m = - { model - | event = res.event - , checkResult = Just res - } + { model | checkResult = Just res } in - ( m, Cmd.none, res.event ) + ( m + , Cmd.none + , if res.success then + Valid event - CheckInputMsg (Err err) -> + else + Invalid event + ) + + CheckInputMsg event (Err err) -> let emptyResult = Api.Model.CalEventCheckResult.empty m = { model - | event = Nothing - , checkResult = + | checkResult = Just { emptyResult | success = False @@ -157,14 +115,14 @@ update flags msg model = } } in - ( m, Cmd.none, Nothing ) + ( m, Cmd.none, Unknown event ) -view : String -> Model -> Html Msg -view extraClasses model = +view : String -> CalEvent -> Model -> Html Msg +view extraClasses ev model = let yearLen = - Basics.max 4 (String.length model.year) + Basics.max 4 (String.length ev.year) otherLen str = Basics.max 2 (String.length str) @@ -181,10 +139,10 @@ view extraClasses model = [ type_ "text" , class "time-input" , size - (Maybe.map otherLen model.weekday + (Maybe.map otherLen ev.weekday |> Maybe.withDefault 4 ) - , Maybe.withDefault "" model.weekday + , Maybe.withDefault "" ev.weekday |> value , onInput SetWeekday ] @@ -196,7 +154,7 @@ view extraClasses model = [ type_ "text" , class "time-input" , size yearLen - , value model.year + , value ev.year , onInput SetYear ] [] @@ -209,8 +167,8 @@ view extraClasses model = , input [ type_ "text" , class "time-input" - , size (otherLen model.month) - , value model.month + , size (otherLen ev.month) + , value ev.month , onInput SetMonth ] [] @@ -223,8 +181,8 @@ view extraClasses model = , input [ type_ "text" , class "time-input" - , size (otherLen model.day) - , value model.day + , size (otherLen ev.day) + , value ev.day , onInput SetDay ] [] @@ -237,8 +195,8 @@ view extraClasses model = , input [ type_ "text" , class "time-input" - , size (otherLen model.hour) - , value model.hour + , size (otherLen ev.hour) + , value ev.hour , onInput SetHour ] [] @@ -251,8 +209,8 @@ view extraClasses model = , input [ type_ "text" , class "time-input" - , size (otherLen model.minute) - , value model.minute + , size (otherLen ev.minute) + , value ev.minute , onInput SetMinute ] [] diff --git a/modules/webapp/src/main/elm/Comp/NotificationForm.elm b/modules/webapp/src/main/elm/Comp/NotificationForm.elm index 90d4faea..df88e333 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationForm.elm @@ -15,7 +15,9 @@ import Comp.CalEventInput import Comp.Dropdown import Comp.EmailInput import Comp.IntField +import Data.CalEvent exposing (CalEvent) import Data.Flags exposing (Flags) +import Data.Validated exposing (Validated) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onCheck, onClick) @@ -35,7 +37,7 @@ type alias Model = , remindDays : Maybe Int , remindDaysModel : Comp.IntField.Model , enabled : Bool - , schedule : String + , schedule : Validated CalEvent , scheduleModel : Comp.CalEventInput.Model , formError : Maybe String } @@ -52,6 +54,7 @@ type Msg | RemindDaysMsg Comp.IntField.Msg | ToggleEnabled | CalEventMsg Comp.CalEventInput.Msg + | SetNotificationSettings (Result Http.Error NotificationSettings) initCmd : Flags -> Cmd Msg @@ -59,14 +62,18 @@ initCmd flags = Cmd.batch [ Api.getMailSettings flags "" ConnResp , Api.getTags flags "" GetTagsResp + , Api.getNotifyDueItems flags SetNotificationSettings ] init : Flags -> ( Model, Cmd Msg ) init flags = let + initialSchedule = + Data.Validated.Unknown Data.CalEvent.everyMonth + ( sm, sc ) = - Comp.CalEventInput.init flags + Comp.CalEventInput.init flags Data.CalEvent.everyMonth in ( { settings = Api.Model.NotificationSettings.empty , connectionModel = @@ -81,7 +88,7 @@ init flags = , remindDays = Just 1 , remindDaysModel = Comp.IntField.init (Just 1) Nothing True "Remind Days" , enabled = False - , schedule = Comp.CalEventInput.initialSchedule + , schedule = initialSchedule , scheduleModel = sm , formError = Nothing } @@ -98,10 +105,13 @@ update flags msg model = CalEventMsg lmsg -> let ( cm, cc, cs ) = - Comp.CalEventInput.update flags lmsg model.scheduleModel + Comp.CalEventInput.update flags + (Data.Validated.value model.schedule) + lmsg + model.scheduleModel in ( { model - | schedule = Maybe.withDefault model.schedule cs + | schedule = cs , scheduleModel = cm } , Cmd.map CalEventMsg cc @@ -141,7 +151,7 @@ update flags msg model = | connectionModel = cm , formError = if names == [] then - Just "No E-Mail connections configured. Goto user settings to add one." + Just "No E-Mail connections configured. Goto E-Mail Settings to add one." else Nothing @@ -199,7 +209,40 @@ update flags msg model = ToggleEnabled -> ( { model | enabled = not model.enabled }, Cmd.none ) - _ -> + SetNotificationSettings (Ok s) -> + let + ( nm, nc ) = + Util.Update.andThen1 + [ update flags (ConnMsg (Comp.Dropdown.SetSelection [ s.smtpConnection ])) + , update flags (TagIncMsg (Comp.Dropdown.SetSelection s.tagsInclude)) + , update flags (TagExcMsg (Comp.Dropdown.SetSelection s.tagsExclude)) + ] + model + + newSchedule = + Data.CalEvent.fromEvent s.schedule + |> Maybe.withDefault Data.CalEvent.everyMonth + + ( sm, sc ) = + Comp.CalEventInput.init flags newSchedule + in + ( { nm + | settings = s + , recipients = s.recipients + , remindDays = Just s.remindDays + , enabled = s.enabled + , schedule = Data.Validated.Unknown newSchedule + } + , Cmd.batch + [ nc + , Cmd.map CalEventMsg sc + ] + ) + + SetNotificationSettings (Err err) -> + ( { model | formError = Just (Util.Http.errorToString err) }, Cmd.none ) + + Submit -> ( model, Cmd.none ) @@ -275,14 +318,22 @@ view extraClasses model = ] ] , Html.map CalEventMsg - (Comp.CalEventInput.view "" model.scheduleModel) + (Comp.CalEventInput.view "" + (Data.Validated.value model.schedule) + model.scheduleModel + ) , span [ class "small-info" ] [ text "Specify how often and when this task should run. " , text "Use English 3-letter weekdays. Either a single value, " - , text "a list or a '*' (meaning all) is allowed for each part." + , text "a list (ex. 1,2,3), a range (ex. 1..3) or a '*' (meaning all) " + , text "is allowed for each part." ] ] , div [ class "ui divider" ] [] + , div [ class "ui error message" ] + [ Maybe.withDefault "" model.formError + |> text + ] , button [ class "ui primary button" , onClick Submit diff --git a/modules/webapp/src/main/elm/Data/CalEvent.elm b/modules/webapp/src/main/elm/Data/CalEvent.elm new file mode 100644 index 00000000..abfc7634 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/CalEvent.elm @@ -0,0 +1,107 @@ +module Data.CalEvent exposing + ( CalEvent + , everyMonth + , fromEvent + , makeEvent + ) + +import Util.Maybe + + +type alias CalEvent = + { weekday : Maybe String + , year : String + , month : String + , day : String + , hour : String + , minute : String + } + + +everyMonth : CalEvent +everyMonth = + CalEvent Nothing "*" "*" "01" "00" "00" + + +makeEvent : CalEvent -> String +makeEvent ev = + let + datetime = + ev.year + ++ "-" + ++ ev.month + ++ "-" + ++ ev.day + ++ " " + ++ ev.hour + ++ ":" + ++ ev.minute + in + case ev.weekday of + Just wd -> + wd ++ " " ++ datetime + + Nothing -> + datetime + + +fromEvent : String -> Maybe CalEvent +fromEvent str = + let + init = + everyMonth + + parts = + String.split " " str + in + case parts of + wd :: date :: time :: [] -> + Maybe.andThen + (fromDate date) + (fromTime time init) + |> Maybe.map (withWeekday wd) + + date :: time :: [] -> + Maybe.andThen + (fromDate date) + (fromTime time init) + + _ -> + Nothing + + +fromDate : String -> CalEvent -> Maybe CalEvent +fromDate date ev = + let + parts = + String.split "-" date + in + case parts of + y :: m :: d :: [] -> + Just + { ev + | year = y + , month = m + , day = d + } + + _ -> + Nothing + + +fromTime : String -> CalEvent -> Maybe CalEvent +fromTime time ev = + case String.split ":" time of + h :: m :: _ :: [] -> + Just { ev | hour = h, minute = m } + + h :: m :: [] -> + Just { ev | hour = h, minute = m } + + _ -> + Nothing + + +withWeekday : String -> CalEvent -> CalEvent +withWeekday wd ev = + { ev | weekday = Util.Maybe.fromString wd } diff --git a/modules/webapp/src/main/elm/Data/Validated.elm b/modules/webapp/src/main/elm/Data/Validated.elm new file mode 100644 index 00000000..0403faca --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Validated.elm @@ -0,0 +1,20 @@ +module Data.Validated exposing (Validated(..), value) + + +type Validated a + = Valid a + | Invalid a + | Unknown a + + +value : Validated a -> a +value va = + case va of + Valid a -> + a + + Invalid a -> + a + + Unknown a -> + a diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 57c4a058..8bdfec20 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -11,6 +11,7 @@ } .calevent-input input.time-input { border: 0 !important; + font-family: monospace !important; text-align: center; } .calevent-input .separator { @@ -27,6 +28,7 @@ text-align: center; } + .default-layout .right-float { float: right; }