Add support for more generic notification

This is a start to have different kinds of notifications. It is
possible to be notified via e-mail, matrix or gotify. It also extends
the current "periodic query" for due items by allowing notification
over different channels. A "generic periodic query" variant is added
as well.
This commit is contained in:
eikek
2021-11-22 00:22:51 +01:00
parent 93a828720c
commit 4ffc8d1f14
175 changed files with 13041 additions and 599 deletions

View File

@ -0,0 +1,280 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.ChannelForm exposing (..)
import Api.Model.NotificationGotify exposing (NotificationGotify)
import Api.Model.NotificationHttp exposing (NotificationHttp)
import Api.Model.NotificationMail exposing (NotificationMail)
import Api.Model.NotificationMatrix exposing (NotificationMatrix)
import Comp.NotificationGotifyForm
import Comp.NotificationHttpForm
import Comp.NotificationMailForm
import Comp.NotificationMatrixForm
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags)
import Data.NotificationChannel exposing (NotificationChannel)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Messages.Comp.ChannelForm exposing (Texts)
type alias MatrixModel =
{ form : Comp.NotificationMatrixForm.Model
, value : Maybe NotificationMatrix
}
type alias GotifyModel =
{ form : Comp.NotificationGotifyForm.Model
, value : Maybe NotificationGotify
}
type alias MailModel =
{ form : Comp.NotificationMailForm.Model
, value : Maybe NotificationMail
}
type alias HttpModel =
{ form : Comp.NotificationHttpForm.Model
, value : Maybe NotificationHttp
}
type alias RefModel =
{ channelType : ChannelType
}
type Model
= Matrix MatrixModel
| Gotify GotifyModel
| Mail MailModel
| Http HttpModel
| Ref RefModel
type Msg
= MatrixMsg Comp.NotificationMatrixForm.Msg
| GotifyMsg Comp.NotificationGotifyForm.Msg
| MailMsg Comp.NotificationMailForm.Msg
| HttpMsg Comp.NotificationHttpForm.Msg
init : Flags -> ChannelType -> ( Model, Cmd Msg )
init flags ct =
case ct of
Data.ChannelType.Matrix ->
( Matrix
{ form = Comp.NotificationMatrixForm.init
, value = Nothing
}
, Cmd.none
)
Data.ChannelType.Gotify ->
( Gotify
{ form = Comp.NotificationGotifyForm.init
, value = Nothing
}
, Cmd.none
)
Data.ChannelType.Mail ->
let
( mm, mc ) =
Comp.NotificationMailForm.init flags
in
( Mail
{ form = mm
, value = Nothing
}
, Cmd.map MailMsg mc
)
Data.ChannelType.Http ->
( Http
{ form = Comp.NotificationHttpForm.init
, value = Nothing
}
, Cmd.none
)
initWith : Flags -> NotificationChannel -> ( Model, Cmd Msg )
initWith flags channel =
case channel of
Data.NotificationChannel.Matrix m ->
( Matrix
{ form = Comp.NotificationMatrixForm.initWith m
, value = Just m
}
, Cmd.none
)
Data.NotificationChannel.Gotify m ->
( Gotify
{ form = Comp.NotificationGotifyForm.initWith m
, value = Just m
}
, Cmd.none
)
Data.NotificationChannel.Mail m ->
let
( mm, mc ) =
Comp.NotificationMailForm.initWith flags m
in
( Mail
{ form = mm
, value = Just m
}
, Cmd.map MailMsg mc
)
Data.NotificationChannel.Http m ->
( Http
{ form = Comp.NotificationHttpForm.initWith m
, value = Just m
}
, Cmd.none
)
Data.NotificationChannel.Ref m ->
( Ref { channelType = m.channelType }
, Cmd.none
)
channelType : Model -> ChannelType
channelType model =
case model of
Matrix _ ->
Data.ChannelType.Matrix
Gotify _ ->
Data.ChannelType.Gotify
Mail _ ->
Data.ChannelType.Mail
Http _ ->
Data.ChannelType.Http
Ref ref ->
ref.channelType
getChannel : Model -> Maybe NotificationChannel
getChannel model =
case model of
Matrix mm ->
Maybe.map Data.NotificationChannel.Matrix mm.value
Gotify mm ->
Maybe.map Data.NotificationChannel.Gotify mm.value
Mail mm ->
Maybe.map Data.NotificationChannel.Mail mm.value
Http mm ->
Maybe.map Data.NotificationChannel.Http mm.value
Ref _ ->
Nothing
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
MatrixMsg lm ->
case model of
Matrix matrix ->
let
( mm, mv ) =
Comp.NotificationMatrixForm.update lm matrix.form
in
( Matrix { form = mm, value = mv }, Cmd.none )
_ ->
( model, Cmd.none )
GotifyMsg lm ->
case model of
Gotify gotify ->
let
( mm, mv ) =
Comp.NotificationGotifyForm.update lm gotify.form
in
( Gotify { form = mm, value = mv }, Cmd.none )
_ ->
( model, Cmd.none )
MailMsg lm ->
case model of
Mail mail ->
let
( mm, mc, mv ) =
Comp.NotificationMailForm.update flags lm mail.form
in
( Mail { form = mm, value = mv }, Cmd.map MailMsg mc )
_ ->
( model, Cmd.none )
HttpMsg lm ->
case model of
Http http ->
let
( mm, mv ) =
Comp.NotificationHttpForm.update lm http.form
in
( Http { form = mm, value = mv }, Cmd.none )
_ ->
( model, Cmd.none )
--- View
view : Texts -> UiSettings -> Model -> Html Msg
view texts settings model =
case model of
Matrix m ->
Html.map MatrixMsg
(Comp.NotificationMatrixForm.view texts.matrixForm m.form)
Gotify m ->
Html.map GotifyMsg
(Comp.NotificationGotifyForm.view texts.gotifyForm m.form)
Mail m ->
Html.map MailMsg
(Comp.NotificationMailForm.view texts.mailForm settings m.form)
Http m ->
Html.map HttpMsg
(Comp.NotificationHttpForm.view texts.httpForm m.form)
-- Note: currently when retrieving hooks, this is not
-- send from the server. The server always sends
-- concrete channel details. However, it is possible
-- to create hooks with a reference to an existing
-- channel, but this is not supported in this client.
-- So this channel is ignored here.
Ref _ ->
span [ class "hidden" ] []

View File

@ -0,0 +1,48 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.ChannelMenu exposing (..)
import Comp.MenuBar as MB
import Data.ChannelType exposing (ChannelType)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Data.ChannelType exposing (Texts)
import Styles as S
type alias Model msg =
{ menuOpen : Bool
, toggleMenu : msg
, menuLabel : String
, onItem : ChannelType -> msg
}
channelMenu : Texts -> Model msg -> MB.Item msg
channelMenu texts model =
MB.Dropdown
{ linkIcon = "fa fa-plus"
, label = model.menuLabel
, linkClass = [ ( S.primaryButton, True ) ]
, toggleMenu = model.toggleMenu
, menuOpen = model.menuOpen
, items =
List.map (menuItem texts model) Data.ChannelType.all
}
menuItem : Texts -> Model msg -> ChannelType -> MB.DropdownMenu msg
menuItem texts model ct =
{ icon = Data.ChannelType.icon ct "w-6 h-6 text-center inline-block"
, label = texts ct
, attrs =
[ href ""
, onClick (model.onItem ct)
]
}

View File

@ -14,6 +14,7 @@ module Comp.Dropdown exposing
, isDropdownChangeMsg
, makeModel
, makeMultiple
, makeMultipleList
, makeSingle
, makeSingleList
, mkOption
@ -116,6 +117,26 @@ makeMultiple =
}
makeMultipleList :
{ options : List a
, selected : List a
}
-> Model a
makeMultipleList opts =
let
m =
makeMultiple
m2 =
{ m | available = List.map (makeItem m) opts.options }
m3 =
List.map (makeItem m2) opts.selected
|> List.foldl (\el -> \model -> selectItem model el) m2
in
m3
getSelected : Model a -> List a
getSelected model =
List.map .value model.selected

View File

@ -5,7 +5,7 @@
-}
module Comp.NotificationForm exposing
module Comp.DueItemsTaskForm exposing
( Action(..)
, Model
, Msg
@ -17,19 +17,21 @@ module Comp.NotificationForm exposing
import Api
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
import Api.Model.NotificationSettings exposing (NotificationSettings)
import Api.Model.Tag exposing (Tag)
import Api.Model.TagList exposing (TagList)
import Comp.Basic as B
import Comp.CalEventInput
import Comp.ChannelForm
import Comp.Dropdown
import Comp.EmailInput
import Comp.IntField
import Comp.MenuBar as MB
import Comp.YesNoDimmer
import Data.CalEvent exposing (CalEvent)
import Data.ChannelType exposing (ChannelType)
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.NotificationChannel
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Data.TagOrder
import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..))
@ -38,7 +40,7 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Http
import Markdown
import Messages.Comp.NotificationForm exposing (Texts)
import Messages.Comp.DueItemsTaskForm exposing (Texts)
import Styles as S
import Util.Maybe
import Util.Tag
@ -46,12 +48,10 @@ import Util.Update
type alias Model =
{ settings : NotificationSettings
, connectionModel : Comp.Dropdown.Model String
{ settings : PeriodicDueItemsSettings
, channelModel : Comp.ChannelForm.Model
, tagInclModel : Comp.Dropdown.Model Tag
, tagExclModel : Comp.Dropdown.Model Tag
, recipients : List String
, recipientsModel : Comp.EmailInput.Model
, remindDays : Maybe Int
, remindDaysModel : Comp.IntField.Model
, capOverdue : Bool
@ -72,15 +72,14 @@ type FormState
type ValidateError
= ValidateConnectionMissing
| ValidateRemindDaysRequired
| ValidateRecipientsRequired
= ValidateRemindDaysRequired
| ValidateCalEventInvalid
| ValidateChannelRequired
type Action
= SubmitAction NotificationSettings
| StartOnceAction NotificationSettings
= SubmitAction PeriodicDueItemsSettings
| StartOnceAction PeriodicDueItemsSettings
| CancelAction
| DeleteAction String
| NoAction
@ -90,9 +89,6 @@ type Msg
= Submit
| TagIncMsg (Comp.Dropdown.Msg Tag)
| TagExcMsg (Comp.Dropdown.Msg Tag)
| ConnMsg (Comp.Dropdown.Msg String)
| ConnResp (Result Http.Error EmailSettingsList)
| RecipientMsg Comp.EmailInput.Msg
| GetTagsResp (Result Http.Error TagList)
| RemindDaysMsg Comp.IntField.Msg
| ToggleEnabled
@ -103,26 +99,25 @@ type Msg
| RequestDelete
| YesNoDeleteMsg Comp.YesNoDimmer.Msg
| SetSummary String
| ChannelMsg Comp.ChannelForm.Msg
initWith : Flags -> NotificationSettings -> ( Model, Cmd Msg )
initWith : Flags -> PeriodicDueItemsSettings -> ( Model, Cmd Msg )
initWith flags s =
let
( im, ic ) =
init flags
ct =
Data.NotificationChannel.channelType s.channel
|> Maybe.withDefault Data.ChannelType.Matrix
smtp =
Util.Maybe.fromString s.smtpConnection
|> Maybe.map List.singleton
|> Maybe.withDefault []
( im, ic ) =
init flags ct
removeAction ( tm, _, tc ) =
( tm, tc )
( nm, nc ) =
Util.Update.andThen1
[ update flags (ConnMsg (Comp.Dropdown.SetSelection smtp)) >> removeAction
, update flags (TagIncMsg (Comp.Dropdown.SetSelection s.tagsInclude)) >> removeAction
[ update flags (TagIncMsg (Comp.Dropdown.SetSelection s.tagsInclude)) >> removeAction
, update flags (TagExcMsg (Comp.Dropdown.SetSelection s.tagsExclude)) >> removeAction
]
im
@ -133,10 +128,13 @@ initWith flags s =
( sm, sc ) =
Comp.CalEventInput.init flags newSchedule
( cfm, cfc ) =
Comp.ChannelForm.initWith flags s.channel
in
( { nm
| settings = s
, recipients = s.recipients
, channelModel = cfm
, remindDays = Just s.remindDays
, enabled = s.enabled
, capOverdue = s.capOverdue
@ -151,25 +149,27 @@ initWith flags s =
[ nc
, ic
, Cmd.map CalEventMsg sc
, Cmd.map ChannelMsg cfc
]
)
init : Flags -> ( Model, Cmd Msg )
init flags =
init : Flags -> ChannelType -> ( Model, Cmd Msg )
init flags ct =
let
initialSchedule =
Data.CalEvent.everyMonth
( sm, scmd ) =
Comp.CalEventInput.init flags initialSchedule
( cfm, cfc ) =
Comp.ChannelForm.init flags ct
in
( { settings = Api.Model.NotificationSettings.empty
, connectionModel = Comp.Dropdown.makeSingle
( { settings = Data.PeriodicDueItemsSettings.empty ct
, channelModel = cfm
, tagInclModel = Util.Tag.makeDropdownModel
, tagExclModel = Util.Tag.makeDropdownModel
, recipients = []
, recipientsModel = Comp.EmailInput.init
, remindDays = Just 1
, remindDaysModel = Comp.IntField.init (Just 1) Nothing True
, enabled = False
@ -177,14 +177,14 @@ init flags =
, schedule = Just initialSchedule
, scheduleModel = sm
, formState = FormStateInitial
, loading = 2
, loading = 1
, yesNoDelete = Comp.YesNoDimmer.emptyModel
, summary = Nothing
}
, Cmd.batch
[ Api.getMailSettings flags "" ConnResp
, Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
[ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
, Cmd.map CalEventMsg scmd
, Cmd.map ChannelMsg cfc
]
)
@ -193,25 +193,12 @@ init flags =
--- Update
makeSettings : Model -> Result ValidateError NotificationSettings
makeSettings : Model -> Result ValidateError PeriodicDueItemsSettings
makeSettings model =
let
prev =
model.settings
conn =
Comp.Dropdown.getSelected model.connectionModel
|> List.head
|> Maybe.map Ok
|> Maybe.withDefault (Err ValidateConnectionMissing)
recp =
if List.isEmpty model.recipients then
Err ValidateRecipientsRequired
else
Ok model.recipients
rmdays =
Maybe.map Ok model.remindDays
|> Maybe.withDefault (Err ValidateRemindDaysRequired)
@ -224,27 +211,30 @@ makeSettings model =
Nothing ->
Err ValidateCalEventInvalid
make smtp rec days timer =
channelM =
Result.fromMaybe
ValidateChannelRequired
(Comp.ChannelForm.getChannel model.channelModel)
make days timer channel =
{ prev
| smtpConnection = smtp
, tagsInclude = Comp.Dropdown.getSelected model.tagInclModel
| tagsInclude = Comp.Dropdown.getSelected model.tagInclModel
, tagsExclude = Comp.Dropdown.getSelected model.tagExclModel
, recipients = rec
, remindDays = days
, capOverdue = model.capOverdue
, enabled = model.enabled
, schedule = Data.CalEvent.makeEvent timer
, summary = model.summary
, channel = channel
}
in
Result.map4 make
conn
recp
Result.map3 make
rmdays
schedule_
channelM
withValidSettings : (NotificationSettings -> Action) -> Model -> ( Model, Action, Cmd Msg )
withValidSettings : (PeriodicDueItemsSettings -> Action) -> Model -> ( Model, Action, Cmd Msg )
withValidSettings mkcmd model =
case makeSettings model of
Ok set ->
@ -263,6 +253,16 @@ withValidSettings mkcmd model =
update : Flags -> Msg -> Model -> ( Model, Action, Cmd Msg )
update flags msg model =
case msg of
ChannelMsg lm ->
let
( cfm, cfc ) =
Comp.ChannelForm.update flags lm model.channelModel
in
( { model | channelModel = cfm }
, NoAction
, Cmd.map ChannelMsg cfc
)
CalEventMsg lmsg ->
let
( cm, cc, cs ) =
@ -280,67 +280,6 @@ update flags msg model =
, Cmd.map CalEventMsg cc
)
RecipientMsg m ->
let
( em, ec, rec ) =
Comp.EmailInput.update flags model.recipients m model.recipientsModel
in
( { model
| recipients = rec
, recipientsModel = em
, formState = FormStateInitial
}
, NoAction
, Cmd.map RecipientMsg ec
)
ConnMsg m ->
let
( cm, cc ) =
Comp.Dropdown.update m model.connectionModel
in
( { model
| connectionModel = cm
, formState = FormStateInitial
}
, NoAction
, Cmd.map ConnMsg cc
)
ConnResp (Ok list) ->
let
names =
List.map .name list.items
cm =
Comp.Dropdown.makeSingleList
{ options = names
, selected = List.head names
}
in
( { model
| connectionModel = cm
, loading = model.loading - 1
, formState =
if names == [] then
FormStateInvalid ValidateConnectionMissing
else
FormStateInitial
}
, NoAction
, Cmd.none
)
ConnResp (Err err) ->
( { model
| formState = FormStateHttpError err
, loading = model.loading - 1
}
, NoAction
, Cmd.none
)
TagIncMsg m ->
let
( m2, c2 ) =
@ -509,12 +448,10 @@ view2 texts extraClasses settings model =
, icon = Just "fa fa-play"
}
connectionCfg =
{ makeOption = \a -> { text = a, additional = "" }
, placeholder = texts.selectConnection
, labelColor = \_ -> \_ -> ""
, style = DS.mainStyle
}
formHeader txt =
h2 [ class S.formHeader, class "mt-2" ]
[ text txt
]
in
div
[ class "flex flex-col md:relative"
@ -539,7 +476,7 @@ view2 texts extraClasses settings model =
}
, MB.SecondaryButton
{ tagger = Cancel
, label = texts.basics.cancel
, label = texts.basics.backToList
, title = texts.basics.backToList
, icon = Just "fa fa-arrow-left"
}
@ -575,17 +512,14 @@ view2 texts extraClasses settings model =
FormStateHttpError err ->
text (texts.httpError err)
FormStateInvalid ValidateConnectionMissing ->
text texts.connectionMissing
FormStateInvalid ValidateCalEventInvalid ->
text texts.invalidCalEvent
FormStateInvalid ValidateRemindDaysRequired ->
text texts.remindDaysRequired
FormStateInvalid ValidateRecipientsRequired ->
text texts.recipientsRequired
FormStateInvalid ValidateChannelRequired ->
text texts.channelRequired
]
, div [ class "mb-4" ]
[ MB.viewItem <|
@ -613,37 +547,11 @@ view2 texts extraClasses settings model =
]
]
, div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ text texts.sendVia
, B.inputRequired
]
, Html.map ConnMsg
(Comp.Dropdown.view2
connectionCfg
settings
model.connectionModel
)
, span [ class "opacity-50 text-sm" ]
[ text texts.sendViaInfo
]
]
, div [ class "mb-4" ]
[ label
[ class S.inputLabel
]
[ text texts.recipients
, B.inputRequired
]
, Html.map RecipientMsg
(Comp.EmailInput.view2
{ style = DS.mainStyle, placeholder = texts.recipients }
model.recipients
model.recipientsModel
)
, span [ class "opacity-50 text-sm" ]
[ text texts.recipientsInfo
]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel))
, Html.map ChannelMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel)
]
, formHeader texts.queryLabel
, div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ text texts.tagsInclude ]
@ -666,7 +574,7 @@ view2 texts extraClasses settings model =
settings
model.tagExclModel
)
, span [ class "small-info" ]
, span [ class "opacity-50 text-sm" ]
[ text texts.tagsExcludeInfo
]
]
@ -692,7 +600,8 @@ view2 texts extraClasses settings model =
]
]
, div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ formHeader texts.schedule
, label [ class S.inputLabel ]
[ text texts.schedule
, a
[ class "float-right"

View File

@ -5,7 +5,7 @@
-}
module Comp.NotificationList exposing
module Comp.DueItemsTaskList exposing
( Action(..)
, Model
, Msg
@ -14,11 +14,13 @@ module Comp.NotificationList exposing
, view2
)
import Api.Model.NotificationSettings exposing (NotificationSettings)
import Comp.Basic as B
import Data.ChannelType
import Data.NotificationChannel
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Messages.Comp.NotificationTable exposing (Texts)
import Messages.Comp.DueItemsTaskList exposing (Texts)
import Styles as S
import Util.Html
@ -28,12 +30,12 @@ type alias Model =
type Msg
= EditSettings NotificationSettings
= EditSettings PeriodicDueItemsSettings
type Action
= NoAction
| EditAction NotificationSettings
| EditAction PeriodicDueItemsSettings
init : Model
@ -52,7 +54,7 @@ update msg model =
--- View2
view2 : Texts -> Model -> List NotificationSettings -> Html Msg
view2 : Texts -> Model -> List PeriodicDueItemsSettings -> Html Msg
view2 texts _ items =
div []
[ table [ class S.tableMain ]
@ -67,8 +69,6 @@ view2 texts _ items =
[ text texts.schedule ]
, th [ class "text-left mr-2" ]
[ text texts.connection ]
, th [ class "text-left hidden sm:table-cell mr-2" ]
[ text texts.recipients ]
]
]
, tbody []
@ -77,7 +77,7 @@ view2 texts _ items =
]
viewItem2 : Texts -> NotificationSettings -> Html Msg
viewItem2 : Texts -> PeriodicDueItemsSettings -> Html Msg
viewItem2 texts item =
tr []
[ B.editLinkTableCell texts.basics.edit (EditSettings item)
@ -94,9 +94,9 @@ viewItem2 texts item =
]
]
, td [ class "text-left mr-2" ]
[ text item.smtpConnection
]
, td [ class "text-left hidden sm:table-cell mr-2" ]
[ String.join ", " item.recipients |> text
[ Data.NotificationChannel.channelType item.channel
|> Maybe.map Data.ChannelType.asString
|> Maybe.withDefault "-"
|> text
]
]

View File

@ -5,7 +5,7 @@
-}
module Comp.NotificationManage exposing
module Comp.DueItemsTaskManage exposing
( Model
, Msg
, init
@ -15,25 +15,27 @@ module Comp.NotificationManage exposing
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.NotificationSettings exposing (NotificationSettings)
import Api.Model.NotificationSettingsList exposing (NotificationSettingsList)
import Comp.ChannelMenu
import Comp.DueItemsTaskForm
import Comp.DueItemsTaskList
import Comp.MenuBar as MB
import Comp.NotificationForm
import Comp.NotificationList
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags)
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Http
import Messages.Comp.NotificationManage exposing (Texts)
import Messages.Comp.DueItemsTaskManage exposing (Texts)
import Styles as S
type alias Model =
{ listModel : Comp.NotificationList.Model
, detailModel : Maybe Comp.NotificationForm.Model
, items : List NotificationSettings
{ listModel : Comp.DueItemsTaskList.Model
, detailModel : Maybe Comp.DueItemsTaskForm.Model
, items : List PeriodicDueItemsSettings
, formState : FormState
, channelMenuOpen : Bool
}
@ -52,19 +54,21 @@ type FormState
type Msg
= ListMsg Comp.NotificationList.Msg
| DetailMsg Comp.NotificationForm.Msg
| GetDataResp (Result Http.Error NotificationSettingsList)
| NewTask
= ListMsg Comp.DueItemsTaskList.Msg
| DetailMsg Comp.DueItemsTaskForm.Msg
| GetDataResp (Result Http.Error (List PeriodicDueItemsSettings))
| NewTaskInit ChannelType
| SubmitResp SubmitType (Result Http.Error BasicResult)
| ToggleChannelMenu
initModel : Model
initModel =
{ listModel = Comp.NotificationList.init
{ listModel = Comp.DueItemsTaskList.init
, detailModel = Nothing
, items = []
, formState = FormStateInitial
, channelMenuOpen = False
}
@ -85,9 +89,14 @@ init flags =
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
GetDataResp (Ok res) ->
ToggleChannelMenu ->
( { model | channelMenuOpen = not model.channelMenuOpen }
, Cmd.none
)
GetDataResp (Ok items) ->
( { model
| items = res.items
| items = items
, formState = FormStateInitial
}
, Cmd.none
@ -101,17 +110,17 @@ update flags msg model =
ListMsg lm ->
let
( mm, action ) =
Comp.NotificationList.update lm model.listModel
Comp.DueItemsTaskList.update lm model.listModel
( detail, cmd ) =
case action of
Comp.NotificationList.NoAction ->
Comp.DueItemsTaskList.NoAction ->
( Nothing, Cmd.none )
Comp.NotificationList.EditAction settings ->
Comp.DueItemsTaskList.EditAction settings ->
let
( dm, dc ) =
Comp.NotificationForm.initWith flags settings
Comp.DueItemsTaskForm.initWith flags settings
in
( Just dm, Cmd.map DetailMsg dc )
in
@ -127,11 +136,11 @@ update flags msg model =
Just dm ->
let
( mm, action, mc ) =
Comp.NotificationForm.update flags lm dm
Comp.DueItemsTaskForm.update flags lm dm
( model_, cmd_ ) =
case action of
Comp.NotificationForm.NoAction ->
Comp.DueItemsTaskForm.NoAction ->
( { model
| detailModel = Just mm
, formState = FormStateInitial
@ -139,7 +148,7 @@ update flags msg model =
, Cmd.none
)
Comp.NotificationForm.SubmitAction settings ->
Comp.DueItemsTaskForm.SubmitAction settings ->
( { model
| detailModel = Just mm
, formState = FormStateInitial
@ -151,7 +160,7 @@ update flags msg model =
Api.updateNotifyDueItems flags settings (SubmitResp SubmitUpdate)
)
Comp.NotificationForm.CancelAction ->
Comp.DueItemsTaskForm.CancelAction ->
( { model
| detailModel = Nothing
, formState = FormStateInitial
@ -159,7 +168,7 @@ update flags msg model =
, initCmd flags
)
Comp.NotificationForm.StartOnceAction settings ->
Comp.DueItemsTaskForm.StartOnceAction settings ->
( { model
| detailModel = Just mm
, formState = FormStateInitial
@ -167,7 +176,7 @@ update flags msg model =
, Api.startOnceNotifyDueItems flags settings (SubmitResp SubmitStartOnce)
)
Comp.NotificationForm.DeleteAction id ->
Comp.DueItemsTaskForm.DeleteAction id ->
( { model
| detailModel = Just mm
, formState = FormStateInitial
@ -185,12 +194,12 @@ update flags msg model =
Nothing ->
( model, Cmd.none )
NewTask ->
NewTaskInit ct ->
let
( mm, mc ) =
Comp.NotificationForm.init flags
Comp.DueItemsTaskForm.init flags ct
in
( { model | detailModel = Just mm }, Cmd.map DetailMsg mc )
( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc )
SubmitResp submitType (Ok res) ->
( { model
@ -277,29 +286,32 @@ isSuccess state =
False
viewForm2 : Texts -> UiSettings -> Comp.NotificationForm.Model -> List (Html Msg)
viewForm2 : Texts -> UiSettings -> Comp.DueItemsTaskForm.Model -> List (Html Msg)
viewForm2 texts settings model =
[ Html.map DetailMsg
(Comp.NotificationForm.view2 texts.notificationForm "flex flex-col" settings model)
(Comp.DueItemsTaskForm.view2 texts.notificationForm "flex flex-col" settings model)
]
viewList2 : Texts -> Model -> List (Html Msg)
viewList2 texts model =
let
menuModel =
{ menuOpen = model.channelMenuOpen
, toggleMenu = ToggleChannelMenu
, menuLabel = texts.newTask
, onItem = NewTaskInit
}
in
[ MB.view
{ start =
[ MB.PrimaryButton
{ tagger = NewTask
, label = texts.newTask
, icon = Just "fa fa-plus"
, title = texts.createNewTask
}
{ start = []
, end =
[ Comp.ChannelMenu.channelMenu texts.channelType menuModel
]
, end = []
, rootClasses = "mb-4"
}
, Html.map ListMsg
(Comp.NotificationList.view2 texts.notificationTable
(Comp.DueItemsTaskList.view2 texts.notificationTable
model.listModel
model.items
)

View File

@ -0,0 +1,182 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.EventSample exposing (Model, Msg, init, initWith, update, viewJson, viewMessage)
import Api
import Comp.FixedDropdown
import Data.DropdownStyle as DS
import Data.EventType exposing (EventType)
import Data.Flags exposing (Flags)
import Html exposing (..)
import Html.Attributes exposing (..)
import Http
import Json.Decode as D
import Json.Print
import Markdown
import Messages.Comp.EventSample exposing (Texts)
type alias Model =
{ eventTypeDropdown : Comp.FixedDropdown.Model EventType
, selectedEventType : Maybe EventType
, content : String
}
init : Model
init =
{ eventTypeDropdown = Comp.FixedDropdown.init Data.EventType.all
, selectedEventType = Nothing
, content = ""
}
initWith : Flags -> EventType -> ( Model, Cmd Msg )
initWith flags evt =
( { init | selectedEventType = Just evt }
, Api.sampleEvent flags evt SampleEvent
)
type Msg
= EventTypeMsg (Comp.FixedDropdown.Msg EventType)
| SampleEvent (Result Http.Error String)
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
EventTypeMsg lm ->
let
( evm, evt ) =
Comp.FixedDropdown.update lm model.eventTypeDropdown
sampleCmd =
case evt of
Just ev ->
Api.sampleEvent flags ev SampleEvent
Nothing ->
Cmd.none
in
( { model
| eventTypeDropdown = evm
, selectedEventType = evt
}
, sampleCmd
)
SampleEvent (Ok str) ->
( { model | content = str }, Cmd.none )
SampleEvent (Err err) ->
( model, Cmd.none )
--- View
styleBase : String
styleBase =
"bg-gray-100 dark:bg-bluegray-900 text-gray-900 dark:text-gray-100 text-sm leading-5"
stylePayload : String
stylePayload =
"px-2 font-mono overflow-auto max-h-96 h-full whitespace-pre"
styleMessage : String
styleMessage =
"-my-2 "
jsonPrettyCfg =
{ indent = 2
, columns = 80
}
dropdownCfg texts =
{ display = texts.eventType >> .name
, icon = \_ -> Nothing
, selectPlaceholder = texts.selectEvent
, style = DS.mainStyleWith "w-48"
}
viewJson : Texts -> Model -> Html Msg
viewJson texts model =
let
json =
Result.withDefault ""
(Json.Print.prettyString jsonPrettyCfg model.content)
in
div
[ class "flex flex-col w-full relative"
]
[ div [ class "flex inline-flex items-center absolute top-2 right-4" ]
[ Html.map EventTypeMsg
(Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
False
model.selectedEventType
model.eventTypeDropdown
)
]
, div
[ class "flex pt-5"
, class styleBase
, class stylePayload
, classList [ ( "hidden", json == "" ) ]
]
[ text json
]
]
viewMessage : Texts -> Model -> Html Msg
viewMessage texts model =
let
titleDecoder =
D.at [ "message", "title" ] D.string
bodyDecoder =
D.at [ "message", "body" ] D.string
title =
D.decodeString titleDecoder model.content
body =
D.decodeString bodyDecoder model.content
in
div
[ class "flex flex-col w-full relative"
]
[ div [ class "flex inline-flex items-center absolute top-2 right-4" ]
[ Html.map EventTypeMsg
(Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
False
model.selectedEventType
model.eventTypeDropdown
)
]
, div
[ class "flex flex-col py-5 px-2 markdown-preview"
, class styleBase
]
[ Markdown.toHtml [ class styleMessage ]
(Result.withDefault "" title)
, Markdown.toHtml [ class styleMessage ]
(Result.withDefault "" body)
]
]

View File

@ -217,6 +217,7 @@ attachHeader texts settings model _ attach =
, MB.viewItem <|
MB.Dropdown
{ linkIcon = "fa fa-bars"
, label = ""
, linkClass =
[ ( "ml-2", True )
, ( S.secondaryBasicButton, True )
@ -225,21 +226,21 @@ attachHeader texts settings model _ attach =
, toggleMenu = ToggleAttachmentDropdown
, menuOpen = model.attachmentDropdownOpen
, items =
[ { icon = "fa fa-download"
[ { icon = i [ class "fa fa-download" ] []
, label = texts.downloadFile
, attrs =
[ download attachName
, href fileUrl
]
}
, { icon = "fa fa-file"
, { icon = i [ class "fa fa-file" ] []
, label = texts.renameFile
, attrs =
[ href "#"
, onClick (EditAttachNameStart attach.id)
]
}
, { icon = "fa fa-file-archive"
, { icon = i [ class "fa fa-file-archive" ] []
, label = texts.downloadOriginalArchiveFile
, attrs =
[ href (fileUrl ++ "/archive")
@ -247,7 +248,7 @@ attachHeader texts settings model _ attach =
, classList [ ( "hidden", not hasArchive ) ]
]
}
, { icon = "fa fa-external-link-alt"
, { icon = i [ class "fa fa-external-link-alt" ] []
, label = texts.originalFile
, attrs =
[ href (fileUrl ++ "/original")
@ -257,31 +258,31 @@ attachHeader texts settings model _ attach =
}
, { icon =
if isAttachMetaOpen model attach.id then
"fa fa-toggle-on"
i [ class "fa fa-toggle-on" ] []
else
"fa fa-toggle-off"
i [ class "fa fa-toggle-off" ] []
, label = texts.viewExtractedData
, attrs =
[ onClick (AttachMetaClick attach.id)
, href "#"
]
}
, { icon = "fa fa-redo-alt"
, { icon = i [ class "fa fa-redo-alt" ] []
, label = texts.reprocessFile
, attrs =
[ onClick (RequestReprocessFile attach.id)
, href "#"
]
}
, { icon = Icons.showQr
, { icon = i [ class Icons.showQr ] []
, label = texts.showQrCode
, attrs =
[ onClick (ToggleShowQrAttach attach.id)
, href "#"
]
}
, { icon = "fa fa-trash"
, { icon = i [ class "fa fa-trash" ] []
, label = texts.deleteThisFile
, attrs =
[ onClick (RequestDeleteAttachment attach.id)

View File

@ -8,6 +8,7 @@
module Comp.MenuBar exposing
( ButtonData
, CheckboxData
, DropdownMenu
, Item(..)
, MenuBar
, TextInputData
@ -85,6 +86,7 @@ type alias LabelData =
type alias DropdownData msg =
{ linkIcon : String
, linkClass : List ( String, Bool )
, label : String
, toggleMenu : msg
, menuOpen : Bool
, items : List (DropdownMenu msg)
@ -92,7 +94,7 @@ type alias DropdownData msg =
type alias DropdownMenu msg =
{ icon : String
{ icon : Html msg
, label : String
, attrs : List (Attribute msg)
}
@ -175,11 +177,7 @@ makeDropdown model =
menuItem m =
a
(class itemStyle :: m.attrs)
[ i
[ class m.icon
, classList [ ( "hidden", m.icon == "" ) ]
]
[]
[ m.icon
, span
[ class "ml-2"
, classList [ ( "hidden", m.label == "" ) ]
@ -196,6 +194,13 @@ makeDropdown model =
, onClick model.toggleMenu
]
[ i [ class model.linkIcon ] []
, if model.label == "" then
span [ class "hidden" ] []
else
span [ class "ml-2" ]
[ text model.label
]
]
, div
[ class menuStyle

View File

@ -0,0 +1,117 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationGotifyForm exposing (Model, Msg, init, initWith, update, view)
import Api.Model.NotificationGotify exposing (NotificationGotify)
import Comp.Basic as B
import Data.NotificationChannel
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Messages.Comp.NotificationGotifyForm exposing (Texts)
import Styles as S
type alias Model =
{ hook : NotificationGotify
}
init : Model
init =
{ hook = Data.NotificationChannel.setTypeGotify Api.Model.NotificationGotify.empty
}
initWith : NotificationGotify -> Model
initWith hook =
{ hook = Data.NotificationChannel.setTypeGotify hook
}
type Msg
= SetUrl String
| SetAppKey String
--- Update
update : Msg -> Model -> ( Model, Maybe NotificationGotify )
update msg model =
let
newHook =
updateHook msg model.hook
in
( { model | hook = newHook }, check newHook )
check : NotificationGotify -> Maybe NotificationGotify
check hook =
Just hook
updateHook : Msg -> NotificationGotify -> NotificationGotify
updateHook msg hook =
case msg of
SetUrl s ->
{ hook | url = s }
SetAppKey s ->
{ hook | appKey = s }
--- View
view : Texts -> Model -> Html Msg
view texts model =
div []
[ div
[ class "mb-2"
]
[ label
[ for "gotifyurl"
, class S.inputLabel
]
[ text texts.gotifyUrl
, B.inputRequired
]
, input
[ type_ "text"
, onInput SetUrl
, placeholder texts.gotifyUrl
, value model.hook.url
, name "gotifyurl"
, class S.textInput
]
[]
]
, div
[ class "mb-2"
]
[ label
[ for "appkey"
, class S.inputLabel
]
[ text texts.appKey
, B.inputRequired
]
, input
[ type_ "text"
, onInput SetAppKey
, placeholder texts.appKey
, value model.hook.appKey
, name "appkey"
, class S.textInput
]
[]
]
]

View File

@ -0,0 +1,317 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationHookForm exposing
( Model
, Msg(..)
, channelType
, getHook
, init
, initWith
, update
, view
)
import Comp.Basic as B
import Comp.ChannelForm
import Comp.Dropdown
import Comp.EventSample
import Comp.MenuBar as MB
import Comp.NotificationTest
import Data.ChannelType exposing (ChannelType)
import Data.DropdownStyle as DS
import Data.EventType exposing (EventType)
import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Messages.Comp.NotificationHookForm exposing (Texts)
import Styles as S
import Util.Maybe
type alias Model =
{ hook : NotificationHook
, enabled : Bool
, channelModel : Comp.ChannelForm.Model
, eventsDropdown : Comp.Dropdown.Model EventType
, eventSampleModel : Comp.EventSample.Model
, testDeliveryModel : Comp.NotificationTest.Model
, allEvents : Bool
, eventFilter : Maybe String
}
init : Flags -> ChannelType -> ( Model, Cmd Msg )
init flags ct =
let
( cm, cc ) =
Comp.ChannelForm.init flags ct
( esm, esc ) =
Comp.EventSample.initWith flags Data.EventType.TagsChanged
in
( { hook = Data.NotificationHook.empty ct
, enabled = True
, channelModel = cm
, eventsDropdown =
Comp.Dropdown.makeMultipleList
{ options = Data.EventType.all
, selected = []
}
, eventSampleModel = esm
, testDeliveryModel = Comp.NotificationTest.init
, allEvents = False
, eventFilter = Nothing
}
, Cmd.batch
[ Cmd.map ChannelFormMsg cc
, Cmd.map EventSampleMsg esc
]
)
initWith : Flags -> NotificationHook -> ( Model, Cmd Msg )
initWith flags h =
let
( cm, cc ) =
Comp.ChannelForm.initWith flags h.channel
( esm, esc ) =
Comp.EventSample.initWith flags Data.EventType.TagsChanged
in
( { hook = h
, enabled = h.enabled
, channelModel = cm
, eventsDropdown =
Comp.Dropdown.makeMultipleList
{ options = Data.EventType.all
, selected = h.events
}
, eventSampleModel = esm
, testDeliveryModel = Comp.NotificationTest.init
, allEvents = h.allEvents
, eventFilter = h.eventFilter
}
, Cmd.batch
[ Cmd.map ChannelFormMsg cc
, Cmd.map EventSampleMsg esc
]
)
channelType : Model -> ChannelType
channelType model =
Comp.ChannelForm.channelType model.channelModel
getHook : Model -> Maybe NotificationHook
getHook model =
let
events =
let
ev =
Comp.Dropdown.getSelected model.eventsDropdown
in
if List.isEmpty ev && not model.allEvents then
Nothing
else
Just ev
channel =
Comp.ChannelForm.getChannel model.channelModel
mkHook ev ch =
NotificationHook model.hook.id model.enabled ch model.allEvents model.eventFilter ev
in
Maybe.map2 mkHook events channel
type Msg
= ToggleEnabled
| ChannelFormMsg Comp.ChannelForm.Msg
| EventMsg (Comp.Dropdown.Msg EventType)
| EventSampleMsg Comp.EventSample.Msg
| DeliveryTestMsg Comp.NotificationTest.Msg
| ToggleAllEvents
| SetEventFilter String
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
SetEventFilter str ->
( { model | eventFilter = Util.Maybe.fromString str }, Cmd.none )
ToggleAllEvents ->
( { model | allEvents = not model.allEvents }
, Cmd.none
)
ToggleEnabled ->
( { model | enabled = not model.enabled }
, Cmd.none
)
ChannelFormMsg lm ->
let
( cm, cc ) =
Comp.ChannelForm.update flags lm model.channelModel
in
( { model | channelModel = cm }, Cmd.map ChannelFormMsg cc )
EventMsg lm ->
if model.allEvents then
( model, Cmd.none )
else
let
( em, ec ) =
Comp.Dropdown.update lm model.eventsDropdown
in
( { model | eventsDropdown = em }, Cmd.map EventMsg ec )
EventSampleMsg lm ->
let
( esm, esc ) =
Comp.EventSample.update flags lm model.eventSampleModel
in
( { model | eventSampleModel = esm }, Cmd.map EventSampleMsg esc )
DeliveryTestMsg lm ->
case getHook model of
Just hook ->
let
( ntm, ntc ) =
Comp.NotificationTest.update flags hook lm model.testDeliveryModel
in
( { model | testDeliveryModel = ntm }, Cmd.map DeliveryTestMsg ntc )
Nothing ->
( model, Cmd.none )
--- View
view : Texts -> UiSettings -> Model -> Html Msg
view texts settings model =
let
connectionCfg =
{ makeOption = \a -> { text = (texts.eventType a).name, additional = (texts.eventType a).info }
, placeholder = texts.selectEvents
, labelColor = \_ -> \_ -> ""
, style = DS.mainStyle
}
formHeader txt =
h2 [ class S.formHeader, class "mt-2" ]
[ text txt
]
in
div
[ class "flex flex-col" ]
[ div [ class "mb-4" ]
[ MB.viewItem <|
MB.Checkbox
{ tagger = \_ -> ToggleEnabled
, label = texts.enableDisable
, value = model.enabled
, id = "notify-enabled"
}
]
, div [ class "mb-4" ]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel))
, Html.map ChannelFormMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel)
]
, div [ class "mb-4" ]
[ formHeader texts.events
, MB.viewItem <|
MB.Checkbox
{ tagger = \_ -> ToggleAllEvents
, label = texts.toggleAllEvents
, value = model.allEvents
, id = "notify-on-all-events"
}
]
, div
[ class "mb-4"
, classList [ ( "disabled", model.allEvents ) ]
]
[ label [ class S.inputLabel ]
[ text texts.events
, B.inputRequired
]
, Html.map EventMsg
(Comp.Dropdown.view2
connectionCfg
settings
model.eventsDropdown
)
, span [ class "opacity-50 text-sm" ]
[ text texts.eventsInfo
]
]
, div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ text texts.eventFilter
, a
[ class "float-right"
, class S.link
, href "https://docspell.org/docs/jsonminiquery/"
, target "_blank"
]
[ i [ class "fa fa-question" ] []
, span [ class "pl-2" ]
[ text texts.eventFilterClickForHelp
]
]
]
, input
[ type_ "text"
, onInput SetEventFilter
, class S.textInput
, Maybe.withDefault "" model.eventFilter
|> value
]
[]
, span [ class "opacity-50 text-sm" ]
[ text texts.eventFilterInfo
]
]
, div
[ class "mt-4"
, classList [ ( "hidden", channelType model /= Data.ChannelType.Http ) ]
]
[ h3 [ class S.header3 ]
[ text texts.samplePayload
]
, Html.map EventSampleMsg
(Comp.EventSample.viewJson texts.eventSample model.eventSampleModel)
]
, div
[ class "mt-4"
, classList [ ( "hidden", channelType model == Data.ChannelType.Http ) ]
]
[ formHeader texts.samplePayload
, Html.map EventSampleMsg
(Comp.EventSample.viewMessage texts.eventSample model.eventSampleModel)
]
, div [ class "mt-4" ]
[ formHeader "Test Delviery"
, Html.map DeliveryTestMsg
(Comp.NotificationTest.view
{ runDisabled = getHook model == Nothing }
model.testDeliveryModel
)
]
]

View File

@ -0,0 +1,475 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationHookManage exposing
( Model
, Msg
, init
, update
, view
)
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Comp.Basic as B
import Comp.ChannelMenu
import Comp.MenuBar as MB
import Comp.NotificationHookForm
import Comp.NotificationHookTable
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import Messages.Comp.NotificationHookManage exposing (Texts)
import Styles as S
type alias Model =
{ listModel : Comp.NotificationHookTable.Model
, detailModel : Maybe Comp.NotificationHookForm.Model
, items : List NotificationHook
, deleteConfirm : DeleteConfirm
, loading : Bool
, formState : FormState
, newHookMenuOpen : Bool
, jsonFilterError : Maybe String
}
type DeleteConfirm
= DeleteConfirmOff
| DeleteConfirmOn
type SubmitType
= SubmitDelete
| SubmitUpdate
| SubmitCreate
type FormState
= FormStateInitial
| FormErrorHttp Http.Error
| FormSubmitSuccessful SubmitType
| FormErrorSubmit String
| FormErrorInvalid
type Msg
= TableMsg Comp.NotificationHookTable.Msg
| DetailMsg Comp.NotificationHookForm.Msg
| GetDataResp (Result Http.Error (List NotificationHook))
| ToggleNewHookMenu
| SubmitResp SubmitType (Result Http.Error BasicResult)
| NewHookInit ChannelType
| BackToTable
| Submit
| RequestDelete
| CancelDelete
| DeleteHookNow String
| VerifyFilterResp NotificationHook (Result Http.Error BasicResult)
initModel : Model
initModel =
{ listModel = Comp.NotificationHookTable.init
, detailModel = Nothing
, items = []
, loading = False
, formState = FormStateInitial
, newHookMenuOpen = False
, deleteConfirm = DeleteConfirmOff
, jsonFilterError = Nothing
}
initCmd : Flags -> Cmd Msg
initCmd flags =
Api.getHooks flags GetDataResp
init : Flags -> ( Model, Cmd Msg )
init flags =
( initModel, initCmd flags )
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
VerifyFilterResp hook (Ok res) ->
if res.success then
postHook flags hook model
else
( { model
| loading = False
, formState = FormErrorInvalid
, jsonFilterError = Just res.message
}
, Cmd.none
)
VerifyFilterResp _ (Err err) ->
( { model | formState = FormErrorHttp err }
, Cmd.none
)
GetDataResp (Ok res) ->
( { model
| items = res
, formState = FormStateInitial
}
, Cmd.none
)
GetDataResp (Err err) ->
( { model | formState = FormErrorHttp err }
, Cmd.none
)
TableMsg lm ->
let
( mm, action ) =
Comp.NotificationHookTable.update flags lm model.listModel
( detail, cmd ) =
case action of
Comp.NotificationHookTable.NoAction ->
( Nothing, Cmd.none )
Comp.NotificationHookTable.EditAction hook ->
let
( dm, dc ) =
Comp.NotificationHookForm.initWith flags hook
in
( Just dm, Cmd.map DetailMsg dc )
in
( { model
| listModel = mm
, detailModel = detail
}
, cmd
)
DetailMsg lm ->
case model.detailModel of
Just dm ->
let
( mm, mc ) =
Comp.NotificationHookForm.update flags lm dm
in
( { model | detailModel = Just mm }
, Cmd.map DetailMsg mc
)
Nothing ->
( model, Cmd.none )
ToggleNewHookMenu ->
( { model | newHookMenuOpen = not model.newHookMenuOpen }, Cmd.none )
SubmitResp submitType (Ok res) ->
( { model
| formState =
if res.success then
FormSubmitSuccessful submitType
else
FormErrorSubmit res.message
, detailModel =
if submitType == SubmitDelete then
Nothing
else
model.detailModel
, loading = False
}
, if submitType == SubmitDelete then
initCmd flags
else
Cmd.none
)
SubmitResp _ (Err err) ->
( { model | formState = FormErrorHttp err, loading = False }
, Cmd.none
)
NewHookInit ct ->
let
( mm, mc ) =
Comp.NotificationHookForm.init flags ct
in
( { model | detailModel = Just mm, newHookMenuOpen = False }, Cmd.map DetailMsg mc )
BackToTable ->
( { model | detailModel = Nothing }, initCmd flags )
Submit ->
case model.detailModel of
Just dm ->
case Comp.NotificationHookForm.getHook dm of
Just data ->
case data.eventFilter of
Nothing ->
postHook flags data model
Just jf ->
( { model | loading = True }, Api.verifyJsonFilter flags jf (VerifyFilterResp data) )
Nothing ->
( { model | formState = FormErrorInvalid }, Cmd.none )
Nothing ->
( model, Cmd.none )
RequestDelete ->
( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none )
CancelDelete ->
( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none )
DeleteHookNow id ->
( { model | deleteConfirm = DeleteConfirmOff, loading = True }
, Api.deleteHook flags id (SubmitResp SubmitDelete)
)
postHook : Flags -> NotificationHook -> Model -> ( Model, Cmd Msg )
postHook flags hook model =
if hook.id == "" then
( { model | loading = True }, Api.createHook flags hook (SubmitResp SubmitCreate) )
else
( { model | loading = True }, Api.updateHook flags hook (SubmitResp SubmitUpdate) )
--- View2
view : Texts -> UiSettings -> Model -> Html Msg
view texts settings model =
div [ class "flex flex-col" ]
(case model.detailModel of
Just msett ->
viewForm texts settings model msett
Nothing ->
viewList texts model
)
viewState : Texts -> Model -> Html Msg
viewState texts model =
div
[ classList
[ ( S.errorMessage, model.formState /= FormStateInitial )
, ( S.successMessage, isSuccess model.formState )
, ( "hidden", model.formState == FormStateInitial )
]
, class "mb-2"
]
[ case model.formState of
FormStateInitial ->
text ""
FormSubmitSuccessful SubmitCreate ->
text texts.hookCreated
FormSubmitSuccessful SubmitUpdate ->
text texts.hookUpdated
FormSubmitSuccessful SubmitDelete ->
text texts.hookDeleted
FormErrorSubmit m ->
text m
FormErrorHttp err ->
text (texts.httpError err)
FormErrorInvalid ->
case model.jsonFilterError of
Just m ->
text (texts.invalidJsonFilter m)
Nothing ->
text texts.formInvalid
]
isSuccess : FormState -> Bool
isSuccess state =
case state of
FormSubmitSuccessful _ ->
True
_ ->
False
viewForm : Texts -> UiSettings -> Model -> Comp.NotificationHookForm.Model -> List (Html Msg)
viewForm texts settings outerModel model =
let
newHook =
model.hook.id == ""
headline =
case Comp.NotificationHookForm.channelType model of
Data.ChannelType.Matrix ->
span []
[ text texts.integrate
, a
[ href "https://matrix.org"
, target "_blank"
, class S.link
, class "mx-3"
]
[ i [ class "fa fa-external-link-alt mr-1" ] []
, text "Matrix"
]
, text texts.intoDocspell
]
Data.ChannelType.Mail ->
span []
[ text texts.notifyEmailInfo
]
Data.ChannelType.Gotify ->
span []
[ text texts.integrate
, a
[ href "https://gotify.net"
, target "_blank"
, class S.link
, class "mx-3"
]
[ i [ class "fa fa-external-link-alt mr-1" ] []
, text "Gotify"
]
, text texts.intoDocspell
]
Data.ChannelType.Http ->
span []
[ text texts.postRequestInfo
]
in
[ h1 [ class S.header2 ]
[ Data.ChannelType.icon (Comp.NotificationHookForm.channelType model) "w-8 h-8 inline-block mr-4"
, if newHook then
text texts.addWebhook
else
text texts.updateWebhook
]
, div [ class "pt-2 pb-4 font-medium" ]
[ headline
]
, MB.view
{ start =
[ MB.CustomElement <|
B.primaryButton
{ handler = onClick Submit
, title = texts.basics.submitThisForm
, icon = "fa fa-save"
, label = texts.basics.submit
, disabled = False
, attrs = [ href "#" ]
}
, MB.SecondaryButton
{ tagger = BackToTable
, title = texts.basics.backToList
, icon = Just "fa fa-arrow-left"
, label = texts.basics.backToList
}
]
, end =
if not newHook then
[ MB.DeleteButton
{ tagger = RequestDelete
, title = texts.deleteThisHook
, icon = Just "fa fa-trash"
, label = texts.basics.delete
}
]
else
[]
, rootClasses = "mb-4"
}
, div [ class "mt-2" ]
[ viewState texts outerModel
]
, Html.map DetailMsg
(Comp.NotificationHookForm.view texts.notificationForm settings model)
, B.loadingDimmer
{ active = outerModel.loading
, label = texts.basics.loading
}
, B.contentDimmer
(outerModel.deleteConfirm == DeleteConfirmOn)
(div [ class "flex flex-col" ]
[ div [ class "text-lg" ]
[ i [ class "fa fa-info-circle mr-2" ] []
, text texts.reallyDeleteHook
]
, div [ class "mt-4 flex flex-row items-center" ]
[ B.deleteButton
{ label = texts.basics.yes
, icon = "fa fa-check"
, disabled = False
, handler = onClick (DeleteHookNow model.hook.id)
, attrs = [ href "#" ]
}
, B.secondaryButton
{ label = texts.basics.no
, icon = "fa fa-times"
, disabled = False
, handler = onClick CancelDelete
, attrs = [ href "#", class "ml-2" ]
}
]
]
)
]
viewList : Texts -> Model -> List (Html Msg)
viewList texts model =
let
menuModel =
{ menuOpen = model.newHookMenuOpen
, toggleMenu = ToggleNewHookMenu
, menuLabel = texts.newHook
, onItem = NewHookInit
}
in
[ MB.view
{ start = []
, end =
[ Comp.ChannelMenu.channelMenu texts.channelType menuModel
]
, rootClasses = "mb-4"
}
, Html.map TableMsg
(Comp.NotificationHookTable.view texts.notificationTable
model.listModel
model.items
)
]

View File

@ -0,0 +1,106 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationHookTable exposing
( Action(..)
, Model
, Msg(..)
, init
, update
, view
)
import Comp.Basic as B
import Data.ChannelType
import Data.EventType
import Data.Flags exposing (Flags)
import Data.NotificationChannel
import Data.NotificationHook exposing (NotificationHook)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.NotificationHookTable exposing (Texts)
import Styles as S
import Util.Html
type alias Model =
{}
type Action
= NoAction
| EditAction NotificationHook
init : Model
init =
{}
type Msg
= Select NotificationHook
update : Flags -> Msg -> Model -> ( Model, Action )
update _ msg model =
case msg of
Select hook ->
( model, EditAction hook )
--- View
view : Texts -> Model -> List NotificationHook -> Html Msg
view texts model hooks =
table [ class S.tableMain ]
[ thead []
[ tr []
[ th [ class "" ] []
, th [ class "text-center mr-2" ]
[ i [ class "fa fa-check" ] []
]
, th [ class "text-left" ]
[ text texts.channel
]
, th [ class "text-left hidden sm:table-cell" ]
[ text texts.events
]
]
]
, tbody []
(List.map (renderNotificationHookLine texts model) hooks)
]
renderNotificationHookLine : Texts -> Model -> NotificationHook -> Html Msg
renderNotificationHookLine texts model hook =
let
eventName =
texts.eventType >> .name
in
tr
[ class S.tableRow
]
[ B.editLinkTableCell texts.basics.edit (Select hook)
, td [ class "w-px whitespace-nowrap px-2 text-center" ]
[ Util.Html.checkbox2 hook.enabled
]
, td [ class "text-left py-4 md:py-2" ]
[ Data.NotificationChannel.channelType hook.channel
|> Maybe.map Data.ChannelType.asString
|> Maybe.withDefault "-"
|> text
]
, td [ class "text-left hidden sm:table-cell" ]
[ List.map eventName hook.events
|> String.join ", "
|> text
]
]

View File

@ -0,0 +1,99 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationHttpForm exposing (Model, Msg, init, initWith, update, view)
import Api.Model.NotificationHttp exposing (NotificationHttp)
import Comp.Basic as B
import Data.NotificationChannel
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Messages.Comp.NotificationHttpForm exposing (Texts)
import Styles as S
type alias Model =
{ hook : NotificationHttp
}
init : Model
init =
{ hook =
Data.NotificationChannel.setTypeHttp
Api.Model.NotificationHttp.empty
}
initWith : NotificationHttp -> Model
initWith hook =
{ hook = Data.NotificationChannel.setTypeHttp hook
}
type Msg
= SetUrl String
--- Update
update : Msg -> Model -> ( Model, Maybe NotificationHttp )
update msg model =
let
newHook =
updateHook msg model.hook
in
( { model | hook = newHook }, check newHook )
check : NotificationHttp -> Maybe NotificationHttp
check hook =
if hook.url == "" then
Nothing
else
Just hook
updateHook : Msg -> NotificationHttp -> NotificationHttp
updateHook msg hook =
case msg of
SetUrl s ->
{ hook | url = s }
--- View
view : Texts -> Model -> Html Msg
view texts model =
div []
[ div
[ class "mb-2"
]
[ label
[ for "httpurl"
, class S.inputLabel
]
[ text texts.httpUrl
, B.inputRequired
]
, input
[ type_ "text"
, onInput SetUrl
, placeholder texts.httpUrl
, value model.hook.url
, name "httpurl"
, class S.textInput
]
[]
]
]

View File

@ -0,0 +1,236 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationMailForm exposing (Model, Msg, init, initWith, update, view)
import Api
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
import Api.Model.NotificationMail exposing (NotificationMail)
import Comp.Basic as B
import Comp.Dropdown
import Comp.EmailInput
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.NotificationChannel
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Http
import Messages.Comp.NotificationMailForm exposing (Texts)
import Styles as S
type alias Model =
{ hook : NotificationMail
, connectionModel : Comp.Dropdown.Model String
, recipients : List String
, recipientsModel : Comp.EmailInput.Model
, formState : FormState
}
type FormState
= FormStateInitial
| FormStateHttpError Http.Error
| FormStateInvalid ValidateError
type ValidateError
= ValidateConnectionMissing
init : Flags -> ( Model, Cmd Msg )
init flags =
( { hook = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty
, connectionModel = Comp.Dropdown.makeSingle
, recipients = []
, recipientsModel = Comp.EmailInput.init
, formState = FormStateInitial
}
, Cmd.batch
[ Api.getMailSettings flags "" ConnResp
]
)
initWith : Flags -> NotificationMail -> ( Model, Cmd Msg )
initWith flags hook =
let
( mm, mc ) =
init flags
( cm, _ ) =
Comp.Dropdown.update (Comp.Dropdown.SetSelection [ hook.connection ]) mm.connectionModel
in
( { mm
| hook = Data.NotificationChannel.setTypeMail hook
, recipients = hook.recipients
, connectionModel = cm
}
, mc
)
type Msg
= ConnResp (Result Http.Error EmailSettingsList)
| ConnMsg (Comp.Dropdown.Msg String)
| RecipientMsg Comp.EmailInput.Msg
--- Update
check : Model -> Maybe NotificationMail
check model =
let
formState =
if model.formState == FormStateInitial then
Just ()
else
Nothing
recipients =
if List.isEmpty model.recipients then
Nothing
else
Just model.recipients
connection =
Comp.Dropdown.getSelected model.connectionModel
|> List.head
h =
model.hook
makeHook _ rec conn =
{ h | connection = conn, recipients = rec }
in
Maybe.map3 makeHook formState recipients connection
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe NotificationMail )
update flags msg model =
case msg of
ConnResp (Ok list) ->
let
names =
List.map .name list.items
cm =
Comp.Dropdown.makeSingleList
{ options = names
, selected = List.head names
}
model_ =
{ model
| connectionModel = cm
, formState =
if names == [] then
FormStateInvalid ValidateConnectionMissing
else
FormStateInitial
}
in
( model_
, Cmd.none
, check model_
)
ConnResp (Err err) ->
( { model | formState = FormStateHttpError err }
, Cmd.none
, Nothing
)
ConnMsg lm ->
let
( cm, cc ) =
Comp.Dropdown.update lm model.connectionModel
model_ =
{ model
| connectionModel = cm
, formState = FormStateInitial
}
in
( model_
, Cmd.map ConnMsg cc
, check model_
)
RecipientMsg lm ->
let
( em, ec, rec ) =
Comp.EmailInput.update flags model.recipients lm model.recipientsModel
model_ =
{ model
| recipients = rec
, recipientsModel = em
, formState = FormStateInitial
}
in
( model_
, Cmd.map RecipientMsg ec
, check model_
)
--- View
view : Texts -> UiSettings -> Model -> Html Msg
view texts settings model =
let
connectionCfg =
{ makeOption = \a -> { text = a, additional = "" }
, placeholder = texts.selectConnection
, labelColor = \_ -> \_ -> ""
, style = DS.mainStyle
}
in
div []
[ div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ text texts.sendVia
, B.inputRequired
]
, Html.map ConnMsg
(Comp.Dropdown.view2
connectionCfg
settings
model.connectionModel
)
, span [ class "opacity-50 text-sm" ]
[ text texts.sendViaInfo
]
]
, div [ class "" ]
[ label
[ class S.inputLabel
]
[ text texts.recipients
, B.inputRequired
]
, Html.map RecipientMsg
(Comp.EmailInput.view2
{ style = DS.mainStyle, placeholder = texts.recipients }
model.recipients
model.recipientsModel
)
, span [ class "opacity-50 text-sm" ]
[ text texts.recipientsInfo
]
]
]

View File

@ -0,0 +1,140 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationMatrixForm exposing (Model, Msg, init, initWith, update, view)
import Api.Model.NotificationMatrix exposing (NotificationMatrix)
import Comp.Basic as B
import Data.NotificationChannel
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Messages.Comp.NotificationMatrixForm exposing (Texts)
import Styles as S
type alias Model =
{ hook : NotificationMatrix
}
init : Model
init =
{ hook = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty
}
initWith : NotificationMatrix -> Model
initWith hook =
{ hook = Data.NotificationChannel.setTypeMatrix hook
}
type Msg
= SetHomeServer String
| SetRoomId String
| SetAccessKey String
--- Update
update : Msg -> Model -> ( Model, Maybe NotificationMatrix )
update msg model =
let
newHook =
updateHook msg model.hook
in
( { model | hook = newHook }, check newHook )
check : NotificationMatrix -> Maybe NotificationMatrix
check hook =
Just hook
updateHook : Msg -> NotificationMatrix -> NotificationMatrix
updateHook msg hook =
case msg of
SetHomeServer s ->
{ hook | homeServer = s }
SetRoomId s ->
{ hook | roomId = s }
SetAccessKey s ->
{ hook | accessToken = s }
--- View
view : Texts -> Model -> Html Msg
view texts model =
div []
[ div
[ class "mb-2"
]
[ label
[ for "homeserver"
, class S.inputLabel
]
[ text texts.homeServer
, B.inputRequired
]
, input
[ type_ "text"
, onInput SetHomeServer
, placeholder texts.homeServer
, value model.hook.homeServer
, name "homeserver"
, class S.textInput
]
[]
]
, div
[ class "mb-2"
]
[ label
[ for "roomid"
, class S.inputLabel
]
[ text texts.roomId
, B.inputRequired
]
, input
[ type_ "text"
, onInput SetRoomId
, placeholder texts.roomId
, value model.hook.roomId
, name "roomid"
, class S.textInput
]
[]
]
, div
[ class "mb-2"
]
[ label
[ for "accesskey"
, class S.inputLabel
]
[ text texts.accessKey
, B.inputRequired
]
, textarea
[ onInput SetAccessKey
, placeholder texts.accessKey
, value model.hook.accessToken
, name "accesskey"
, class S.textAreaInput
]
[]
]
]

View File

@ -0,0 +1,146 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.NotificationTest exposing (Model, Msg, ViewConfig, init, update, view)
import Api
import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult)
import Comp.Basic as B
import Comp.MenuBar as MB
import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
type Model
= ModelInit
| ModelResp NotificationChannelTestResult
| ModelHttpError Http.Error
| ModelLoading
init : Model
init =
ModelInit
type Msg
= RunTest
| TestResp (Result Http.Error NotificationChannelTestResult)
hasResponse : Model -> Bool
hasResponse model =
case model of
ModelResp _ ->
True
_ ->
False
--- Update
update : Flags -> NotificationHook -> Msg -> Model -> ( Model, Cmd Msg )
update flags hook msg model =
case msg of
RunTest ->
case model of
ModelLoading ->
( model, Cmd.none )
_ ->
( ModelLoading, Api.testHook flags hook TestResp )
TestResp (Ok res) ->
( ModelResp res, Cmd.none )
TestResp (Err err) ->
( ModelHttpError err, Cmd.none )
--- View
type alias ViewConfig =
{ runDisabled : Bool
}
styleBase : String
styleBase =
"bg-gray-100 dark:bg-bluegray-900 text-gray-900 dark:text-gray-100 text-sm leading-5"
stylePayload : String
stylePayload =
"px-2 font-mono overflow-auto h-full whitespace-pre "
view : ViewConfig -> Model -> Html Msg
view cfg model =
div
[ class "flex flex-col w-full"
]
[ MB.view
{ start =
case model of
ModelResp res ->
[ MB.CustomElement <|
if res.success then
div [ class "text-3xl text-green-500" ]
[ i [ class "fa fa-check" ] []
]
else
div [ class "text-3xl text-red-500" ]
[ i [ class "fa fa-times" ] []
]
]
_ ->
[]
, end =
[ MB.CustomElement <|
B.primaryButton
{ label = "Test Delivery"
, disabled = cfg.runDisabled || model == ModelLoading
, icon =
if model == ModelLoading then
"fa fa-cog animate-spin"
else
"fa fa-cog"
, handler = onClick RunTest
, attrs = [ href "#" ]
}
]
, rootClasses = "mb-1"
}
, case model of
ModelResp res ->
div
[ class "flex flex-col py-5 px-2"
, class styleBase
, class stylePayload
]
[ text (String.join "\n" res.messages)
]
ModelHttpError err ->
div [ class "" ]
[]
_ ->
span [ class "hidden" ] []
]

View File

@ -0,0 +1,484 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.PeriodicQueryTaskForm exposing
( Action(..)
, Model
, Msg
, UpdateResult
, init
, initWith
, update
, view
)
import Comp.Basic as B
import Comp.CalEventInput
import Comp.ChannelForm
import Comp.MenuBar as MB
import Comp.PowerSearchInput
import Data.CalEvent exposing (CalEvent)
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags)
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..))
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Http
import Messages.Comp.PeriodicQueryTaskForm exposing (Texts)
import Styles as S
import Util.Maybe
type alias Model =
{ settings : PeriodicQuerySettings
, enabled : Bool
, summary : Maybe String
, schedule : Maybe CalEvent
, scheduleModel : Comp.CalEventInput.Model
, queryModel : Comp.PowerSearchInput.Model
, channelModel : Comp.ChannelForm.Model
, formState : FormState
, loading : Int
}
type FormState
= FormStateInitial
| FormStateHttpError Http.Error
| FormStateInvalid ValidateError
type ValidateError
= ValidateCalEventInvalid
| ValidateQueryStringRequired
| ValidateChannelRequired
type Action
= SubmitAction PeriodicQuerySettings
| StartOnceAction PeriodicQuerySettings
| CancelAction
| DeleteAction String
| NoAction
type Msg
= Submit
| ToggleEnabled
| CalEventMsg Comp.CalEventInput.Msg
| QueryMsg Comp.PowerSearchInput.Msg
| ChannelMsg Comp.ChannelForm.Msg
| StartOnce
| Cancel
| RequestDelete
| SetSummary String
initWith : Flags -> PeriodicQuerySettings -> ( Model, Cmd Msg )
initWith flags s =
let
newSchedule =
Data.CalEvent.fromEvent s.schedule
|> Maybe.withDefault Data.CalEvent.everyMonth
( sm, sc ) =
Comp.CalEventInput.init flags newSchedule
res =
Comp.PowerSearchInput.update
(Comp.PowerSearchInput.setSearchString s.query)
Comp.PowerSearchInput.init
( cfm, cfc ) =
Comp.ChannelForm.initWith flags s.channel
in
( { settings = s
, enabled = s.enabled
, schedule = Just newSchedule
, scheduleModel = sm
, queryModel = res.model
, channelModel = cfm
, formState = FormStateInitial
, loading = 0
, summary = s.summary
}
, Cmd.batch
[ Cmd.map CalEventMsg sc
, Cmd.map QueryMsg res.cmd
, Cmd.map ChannelMsg cfc
]
)
init : Flags -> ChannelType -> ( Model, Cmd Msg )
init flags ct =
let
initialSchedule =
Data.CalEvent.everyMonth
( sm, scmd ) =
Comp.CalEventInput.init flags initialSchedule
( cfm, cfc ) =
Comp.ChannelForm.init flags ct
in
( { settings = Data.PeriodicQuerySettings.empty ct
, enabled = False
, schedule = Just initialSchedule
, scheduleModel = sm
, queryModel = Comp.PowerSearchInput.init
, channelModel = cfm
, formState = FormStateInitial
, loading = 0
, summary = Nothing
}
, Cmd.batch
[ Cmd.map CalEventMsg scmd
, Cmd.map ChannelMsg cfc
]
)
--- Update
type alias UpdateResult =
{ model : Model
, action : Action
, cmd : Cmd Msg
, sub : Sub Msg
}
makeSettings : Model -> Result ValidateError PeriodicQuerySettings
makeSettings model =
let
prev =
model.settings
schedule_ =
case model.schedule of
Just s ->
Ok s
Nothing ->
Err ValidateCalEventInvalid
queryString =
Result.fromMaybe ValidateQueryStringRequired model.queryModel.input
channelM =
Result.fromMaybe
ValidateChannelRequired
(Comp.ChannelForm.getChannel model.channelModel)
make timer channel query =
{ prev
| enabled = model.enabled
, schedule = Data.CalEvent.makeEvent timer
, summary = model.summary
, channel = channel
, query = query
}
in
Result.map3 make
schedule_
channelM
queryString
withValidSettings : (PeriodicQuerySettings -> Action) -> Model -> UpdateResult
withValidSettings mkcmd model =
case makeSettings model of
Ok set ->
{ model = { model | formState = FormStateInitial }
, action = mkcmd set
, cmd = Cmd.none
, sub = Sub.none
}
Err errs ->
{ model = { model | formState = FormStateInvalid errs }
, action = NoAction
, cmd = Cmd.none
, sub = Sub.none
}
update : Flags -> Msg -> Model -> UpdateResult
update flags msg model =
case msg of
CalEventMsg lmsg ->
let
( cm, cc, cs ) =
Comp.CalEventInput.update flags
model.schedule
lmsg
model.scheduleModel
in
{ model =
{ model
| schedule = cs
, scheduleModel = cm
, formState = FormStateInitial
}
, action = NoAction
, cmd = Cmd.map CalEventMsg cc
, sub = Sub.none
}
QueryMsg lm ->
let
res =
Comp.PowerSearchInput.update lm model.queryModel
in
{ model = { model | queryModel = res.model }
, action = NoAction
, cmd = Cmd.map QueryMsg res.cmd
, sub = Sub.map QueryMsg res.subs
}
ChannelMsg lm ->
let
( cfm, cfc ) =
Comp.ChannelForm.update flags lm model.channelModel
in
{ model = { model | channelModel = cfm }
, action = NoAction
, cmd = Cmd.map ChannelMsg cfc
, sub = Sub.none
}
ToggleEnabled ->
{ model =
{ model
| enabled = not model.enabled
, formState = FormStateInitial
}
, action = NoAction
, cmd = Cmd.none
, sub = Sub.none
}
Submit ->
withValidSettings
SubmitAction
model
StartOnce ->
withValidSettings
StartOnceAction
model
Cancel ->
{ model = model
, action = CancelAction
, cmd = Cmd.none
, sub = Sub.none
}
RequestDelete ->
{ model = model
, action = NoAction
, cmd = Cmd.none
, sub = Sub.none
}
SetSummary str ->
{ model = { model | summary = Util.Maybe.fromString str }
, action = NoAction
, cmd = Cmd.none
, sub = Sub.none
}
--- View2
isFormError : Model -> Bool
isFormError model =
case model.formState of
FormStateInitial ->
False
_ ->
True
isFormSuccess : Model -> Bool
isFormSuccess model =
not (isFormError model)
view : Texts -> String -> UiSettings -> Model -> Html Msg
view texts extraClasses settings model =
let
startOnceBtn =
MB.SecondaryButton
{ tagger = StartOnce
, label = texts.startOnce
, title = texts.startTaskNow
, icon = Just "fa fa-play"
}
queryInput =
div
[ class "relative flex flex-grow flex-row" ]
[ Html.map QueryMsg
(Comp.PowerSearchInput.viewInput
{ placeholder = texts.queryLabel
, extraAttrs = []
}
model.queryModel
)
, Html.map QueryMsg
(Comp.PowerSearchInput.viewResult [] model.queryModel)
]
formHeader txt =
h2 [ class S.formHeader, class "mt-2" ]
[ text txt
]
in
div
[ class "flex flex-col md:relative"
, class extraClasses
]
[ B.loadingDimmer
{ active = model.loading > 0
, label = texts.basics.loading
}
, MB.view
{ start =
[ MB.PrimaryButton
{ tagger = Submit
, label = texts.basics.submit
, title = texts.basics.submitThisForm
, icon = Just "fa fa-save"
}
, MB.SecondaryButton
{ tagger = Cancel
, label = texts.basics.backToList
, title = texts.basics.backToList
, icon = Just "fa fa-arrow-left"
}
]
, end =
if model.settings.id /= "" then
[ startOnceBtn
, MB.DeleteButton
{ tagger = RequestDelete
, label = texts.basics.delete
, title = texts.deleteThisTask
, icon = Just "fa fa-trash"
}
]
else
[ startOnceBtn
]
, rootClasses = "mb-4"
}
, div
[ classList
[ ( S.successMessage, isFormSuccess model )
, ( S.errorMessage, isFormError model )
, ( "hidden", model.formState == FormStateInitial )
]
, class "mb-4"
]
[ case model.formState of
FormStateInitial ->
text ""
FormStateHttpError err ->
text (texts.httpError err)
FormStateInvalid ValidateCalEventInvalid ->
text texts.invalidCalEvent
FormStateInvalid ValidateChannelRequired ->
text texts.channelRequired
FormStateInvalid ValidateQueryStringRequired ->
text texts.queryStringRequired
]
, div [ class "mb-4" ]
[ MB.viewItem <|
MB.Checkbox
{ tagger = \_ -> ToggleEnabled
, label = texts.enableDisable
, value = model.enabled
, id = "notify-enabled"
}
]
, div [ class "mb-4" ]
[ label [ class S.inputLabel ]
[ text texts.summary
]
, input
[ type_ "text"
, onInput SetSummary
, class S.textInput
, Maybe.withDefault "" model.summary
|> value
]
[]
, span [ class "opacity-50 text-sm" ]
[ text texts.summaryInfo
]
]
, div [ class "mb-4" ]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel))
, Html.map ChannelMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel)
]
, div [ class "mb-4" ]
[ formHeader texts.queryLabel
, label
[ for "sharequery"
, class S.inputLabel
]
[ text texts.queryLabel
, B.inputRequired
]
, queryInput
]
, div [ class "mb-4" ]
[ formHeader texts.schedule
, label [ class S.inputLabel ]
[ text texts.schedule
, B.inputRequired
, a
[ class "float-right"
, class S.link
, href "https://github.com/eikek/calev#what-are-calendar-events"
, target "_blank"
]
[ i [ class "fa fa-question" ] []
, span [ class "pl-2" ]
[ text texts.scheduleClickForHelp
]
]
]
, Html.map CalEventMsg
(Comp.CalEventInput.view2
texts.calEventInput
""
model.schedule
model.scheduleModel
)
, span [ class "opacity-50 text-sm" ]
[ text texts.scheduleInfo
]
]
]

View File

@ -0,0 +1,102 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.PeriodicQueryTaskList exposing
( Action(..)
, Model
, Msg
, init
, update
, view2
)
import Comp.Basic as B
import Data.ChannelType
import Data.NotificationChannel
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Messages.Comp.PeriodicQueryTaskList exposing (Texts)
import Styles as S
import Util.Html
type alias Model =
{}
type Msg
= EditSettings PeriodicQuerySettings
type Action
= NoAction
| EditAction PeriodicQuerySettings
init : Model
init =
{}
update : Msg -> Model -> ( Model, Action )
update msg model =
case msg of
EditSettings settings ->
( model, EditAction settings )
--- View2
view2 : Texts -> Model -> List PeriodicQuerySettings -> Html Msg
view2 texts _ items =
div []
[ table [ class S.tableMain ]
[ thead []
[ tr []
[ th [ class "" ] []
, th [ class "text-center mr-2" ]
[ i [ class "fa fa-check" ] []
]
, th [ class "text-left " ] [ text texts.summary ]
, th [ class "text-left hidden sm:table-cell mr-2" ]
[ text texts.schedule ]
, th [ class "text-left mr-2" ]
[ text texts.connection ]
]
]
, tbody []
(List.map (viewItem2 texts) items)
]
]
viewItem2 : Texts -> PeriodicQuerySettings -> Html Msg
viewItem2 texts item =
tr []
[ B.editLinkTableCell texts.basics.edit (EditSettings item)
, td [ class "w-px whitespace-nowrap px-2 text-center" ]
[ Util.Html.checkbox2 item.enabled
]
, td [ class "text-left" ]
[ Maybe.withDefault "" item.summary
|> text
]
, td [ class "text-left hidden sm:table-cell mr-2" ]
[ code [ class "font-mono text-sm" ]
[ text item.schedule
]
]
, td [ class "text-left py-4 md:py-2" ]
[ Data.NotificationChannel.channelType item.channel
|> Maybe.map Data.ChannelType.asString
|> Maybe.withDefault "-"
|> text
]
]

View File

@ -0,0 +1,324 @@
{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Comp.PeriodicQueryTaskManage exposing
( Model
, Msg
, init
, update
, view
)
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Comp.ChannelMenu
import Comp.MenuBar as MB
import Comp.PeriodicQueryTaskForm
import Comp.PeriodicQueryTaskList
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags)
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import Messages.Comp.PeriodicQueryTaskManage exposing (Texts)
import Styles as S
type alias Model =
{ listModel : Comp.PeriodicQueryTaskList.Model
, detailModel : Maybe Comp.PeriodicQueryTaskForm.Model
, items : List PeriodicQuerySettings
, formState : FormState
, channelMenuOpen : Bool
}
type SubmitType
= SubmitDelete
| SubmitUpdate
| SubmitCreate
| SubmitStartOnce
type FormState
= FormStateInitial
| FormHttpError Http.Error
| FormSubmitSuccessful SubmitType
| FormSubmitFailed String
type Msg
= ListMsg Comp.PeriodicQueryTaskList.Msg
| DetailMsg Comp.PeriodicQueryTaskForm.Msg
| GetDataResp (Result Http.Error (List PeriodicQuerySettings))
| NewTaskInit ChannelType
| SubmitResp SubmitType (Result Http.Error BasicResult)
| ToggleChannelMenu
initModel : Model
initModel =
{ listModel = Comp.PeriodicQueryTaskList.init
, detailModel = Nothing
, items = []
, formState = FormStateInitial
, channelMenuOpen = False
}
initCmd : Flags -> Cmd Msg
initCmd flags =
Api.getPeriodicQuery flags GetDataResp
init : Flags -> ( Model, Cmd Msg )
init flags =
( initModel, initCmd flags )
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
update flags msg model =
case msg of
GetDataResp (Ok items) ->
( { model
| items = items
, formState = FormStateInitial
}
, Cmd.none
, Sub.none
)
GetDataResp (Err err) ->
( { model | formState = FormHttpError err }
, Cmd.none
, Sub.none
)
ListMsg lm ->
let
( mm, action ) =
Comp.PeriodicQueryTaskList.update lm model.listModel
( detail, cmd ) =
case action of
Comp.PeriodicQueryTaskList.NoAction ->
( Nothing, Cmd.none )
Comp.PeriodicQueryTaskList.EditAction settings ->
let
( dm, dc ) =
Comp.PeriodicQueryTaskForm.initWith flags settings
in
( Just dm, Cmd.map DetailMsg dc )
in
( { model
| listModel = mm
, detailModel = detail
}
, cmd
, Sub.none
)
DetailMsg lm ->
case model.detailModel of
Just dm ->
let
--( mm, action, mc ) =
result =
Comp.PeriodicQueryTaskForm.update flags lm dm
( model_, cmd_ ) =
case result.action of
Comp.PeriodicQueryTaskForm.NoAction ->
( { model
| detailModel = Just result.model
, formState = FormStateInitial
}
, Cmd.none
)
Comp.PeriodicQueryTaskForm.SubmitAction settings ->
( { model
| detailModel = Just result.model
, formState = FormStateInitial
}
, if settings.id == "" then
Api.createPeriodicQuery flags settings (SubmitResp SubmitCreate)
else
Api.updatePeriodicQuery flags settings (SubmitResp SubmitUpdate)
)
Comp.PeriodicQueryTaskForm.CancelAction ->
( { model
| detailModel = Nothing
, formState = FormStateInitial
}
, initCmd flags
)
Comp.PeriodicQueryTaskForm.StartOnceAction settings ->
( { model
| detailModel = Just result.model
, formState = FormStateInitial
}
, Api.startOncePeriodicQuery flags settings (SubmitResp SubmitStartOnce)
)
Comp.PeriodicQueryTaskForm.DeleteAction id ->
( { model
| detailModel = Just result.model
, formState = FormStateInitial
}
, Api.deletePeriodicQueryTask flags id (SubmitResp SubmitDelete)
)
in
( model_
, Cmd.batch
[ Cmd.map DetailMsg result.cmd
, cmd_
]
, Sub.map DetailMsg result.sub
)
Nothing ->
( model, Cmd.none, Sub.none )
NewTaskInit ct ->
let
( mm, mc ) =
Comp.PeriodicQueryTaskForm.init flags ct
in
( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc, Sub.none )
SubmitResp submitType (Ok res) ->
( { model
| formState =
if res.success then
FormSubmitSuccessful submitType
else
FormSubmitFailed res.message
, detailModel =
if submitType == SubmitDelete then
Nothing
else
model.detailModel
}
, if submitType == SubmitDelete then
initCmd flags
else
Cmd.none
, Sub.none
)
SubmitResp _ (Err err) ->
( { model | formState = FormHttpError err }
, Cmd.none
, Sub.none
)
ToggleChannelMenu ->
( { model | channelMenuOpen = not model.channelMenuOpen }, Cmd.none, Sub.none )
--- View2
view : Texts -> UiSettings -> Model -> Html Msg
view texts settings model =
div [ class "flex flex-col" ]
(div
[ classList
[ ( S.errorMessage, model.formState /= FormStateInitial )
, ( S.successMessage, isSuccess model.formState )
, ( "hidden", model.formState == FormStateInitial )
]
, class "mb-2"
]
[ case model.formState of
FormStateInitial ->
text ""
FormSubmitSuccessful SubmitCreate ->
text texts.taskCreated
FormSubmitSuccessful SubmitUpdate ->
text texts.taskUpdated
FormSubmitSuccessful SubmitStartOnce ->
text texts.taskStarted
FormSubmitSuccessful SubmitDelete ->
text texts.taskDeleted
FormSubmitFailed m ->
text m
FormHttpError err ->
text (texts.httpError err)
]
:: (case model.detailModel of
Just msett ->
viewForm2 texts settings msett
Nothing ->
viewList2 texts model
)
)
isSuccess : FormState -> Bool
isSuccess state =
case state of
FormSubmitSuccessful _ ->
True
_ ->
False
viewForm2 : Texts -> UiSettings -> Comp.PeriodicQueryTaskForm.Model -> List (Html Msg)
viewForm2 texts settings model =
[ Html.map DetailMsg
(Comp.PeriodicQueryTaskForm.view texts.notificationForm "flex flex-col" settings model)
]
viewList2 : Texts -> Model -> List (Html Msg)
viewList2 texts model =
let
menuModel =
{ menuOpen = model.channelMenuOpen
, toggleMenu = ToggleChannelMenu
, menuLabel = texts.newTask
, onItem = NewTaskInit
}
in
[ MB.view
{ start = []
, end =
[ Comp.ChannelMenu.channelMenu texts.channelType menuModel
]
, rootClasses = "mb-4"
}
, Html.map ListMsg
(Comp.PeriodicQueryTaskList.view2 texts.notificationTable
model.listModel
model.items
)
]

View File

@ -287,7 +287,8 @@ viewForm2 texts flags settings model =
viewList2 : Texts -> Model -> List (Html Msg)
viewList2 texts model =
[ MB.view
{ start =
{ start = []
, end =
[ MB.PrimaryButton
{ tagger = NewTask
, label = texts.newTask
@ -295,7 +296,6 @@ viewList2 texts model =
, title = texts.createNewTask
}
]
, end = []
, rootClasses = "mb-4"
}
, Html.map ListMsg