diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 919f8a1b..ff22e2a6 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -18,6 +18,14 @@ module Api exposing , addShare , addTag , addTagsMultiple + , addonRunConfigDelete + , addonRunConfigGet + , addonRunConfigSet + , addonRunExistingItem + , addonsDelete + , addonsGetAll + , addonsInstall + , addonsUpdate , attachmentPreviewURL , bookmarkNameExists , cancelJob @@ -211,6 +219,11 @@ module Api exposing , versionInfo ) +import Api.Model.AddonList exposing (AddonList) +import Api.Model.AddonRegister exposing (AddonRegister) +import Api.Model.AddonRunConfig exposing (AddonRunConfig) +import Api.Model.AddonRunConfigList exposing (AddonRunConfigList) +import Api.Model.AddonRunExistingItem exposing (AddonRunExistingItem) import Api.Model.AttachmentMeta exposing (AttachmentMeta) import Api.Model.AuthResult exposing (AuthResult) import Api.Model.BasicResult exposing (BasicResult) @@ -3156,6 +3169,99 @@ shareDownloadAllLink flags id = +--- Addons + + +addonsGetAll : Flags -> (Result Http.Error AddonList -> msg) -> Cmd msg +addonsGetAll flags receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/archive" + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.AddonList.decoder + } + + +addonsDelete : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addonsDelete flags addonId receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/archive/" ++ addonId + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +addonsInstall : Flags -> AddonRegister -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addonsInstall flags addon receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/archive" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.AddonRegister.encode addon) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +addonsUpdate : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addonsUpdate flags addonId receive = + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/archive/" ++ addonId + , account = getAccount flags + , body = Http.emptyBody + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +addonRunConfigGet : Flags -> (Result Http.Error AddonRunConfigList -> msg) -> Cmd msg +addonRunConfigGet flags receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/run-config" + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.AddonRunConfigList.decoder + } + + +addonRunConfigSet : + Flags + -> AddonRunConfig + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +addonRunConfigSet flags cfg receive = + if cfg.id == "" then + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/run-config" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.AddonRunConfig.encode cfg) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + else + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/run-config/" ++ cfg.id + , account = getAccount flags + , body = Http.jsonBody (Api.Model.AddonRunConfig.encode cfg) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +addonRunConfigDelete : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addonRunConfigDelete flags id receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/run-config/" ++ id + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +addonRunExistingItem : Flags -> AddonRunExistingItem -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addonRunExistingItem flags input receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/addon/run/existingitem" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.AddonRunExistingItem.encode input) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + + --- Helper diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 7f98c14c..67b9f416 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -14,6 +14,7 @@ import Api import App.Data exposing (..) import Browser exposing (UrlRequest(..)) import Browser.Navigation as Nav +import Comp.AddonArchiveManage import Comp.DownloadAll import Data.AppEvent exposing (AppEvent(..)) import Data.Environment as Env @@ -345,6 +346,9 @@ updateWithSub msg model = Ok (JobsWaiting n) -> ( { model | jobsWaiting = max 0 n }, Cmd.none, Sub.none ) + Ok (AddonInstalled info) -> + updateManageData (Page.ManageData.Data.AddonArchiveMsg <| Comp.AddonArchiveManage.addonInstallResult info) model + Err _ -> ( model, Cmd.none, Sub.none ) @@ -640,7 +644,7 @@ updateManageData : Page.ManageData.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Ms updateManageData lmsg model = let ( lm, lc, ls ) = - Page.ManageData.Update.update model.flags lmsg model.manageDataModel + Page.ManageData.Update.update model.flags model.uiSettings lmsg model.manageDataModel in ( { model | manageDataModel = lm } , Cmd.map ManageDataMsg lc diff --git a/modules/webapp/src/main/elm/Comp/AddonArchiveForm.elm b/modules/webapp/src/main/elm/Comp/AddonArchiveForm.elm new file mode 100644 index 00000000..5030eef5 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonArchiveForm.elm @@ -0,0 +1,106 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonArchiveForm exposing (Model, Msg, get, init, initWith, update, view) + +import Api.Model.Addon exposing (Addon) +import Comp.Basic as B +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onInput) +import Messages.Comp.AddonArchiveForm exposing (Texts) +import Styles as S +import Util.Maybe + + +type alias Model = + { addon : Addon + , url : Maybe String + } + + +init : ( Model, Cmd Msg ) +init = + ( { addon = Api.Model.Addon.empty + , url = Nothing + } + , Cmd.none + ) + + +initWith : Addon -> ( Model, Cmd Msg ) +initWith a = + ( { addon = a + , url = a.url + } + , Cmd.none + ) + + +isValid : Model -> Bool +isValid model = + model.url /= Nothing + + +get : Model -> Maybe Addon +get model = + let + a = + model.addon + in + if isValid model then + Just + { a + | url = model.url + } + + else + Nothing + + +type Msg + = SetUrl String + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update _ msg model = + case msg of + SetUrl url -> + ( { model | url = Util.Maybe.fromString url }, Cmd.none ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + div + [ class "flex flex-col" ] + [ div [ class "mb-4" ] + [ label + [ class S.inputLabel + ] + [ text texts.addonUrl + , B.inputRequired + ] + , input + [ type_ "text" + , placeholder texts.addonUrlPlaceholder + , class S.textInput + , classList [ ( "disabled", model.addon.id /= "" ) ] + , value (model.url |> Maybe.withDefault "") + , onInput SetUrl + , disabled (model.addon.id /= "") + ] + [] + , span [ class "text-sm opacity-75" ] + [ text texts.installInfoText + ] + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/AddonArchiveManage.elm b/modules/webapp/src/main/elm/Comp/AddonArchiveManage.elm new file mode 100644 index 00000000..68f460f5 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonArchiveManage.elm @@ -0,0 +1,429 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonArchiveManage exposing (Model, Msg, addonInstallResult, init, loadAddons, update, view) + +import Api +import Api.Model.Addon exposing (Addon) +import Api.Model.AddonList exposing (AddonList) +import Api.Model.AddonRegister exposing (AddonRegister) +import Api.Model.BasicResult exposing (BasicResult) +import Comp.AddonArchiveForm +import Comp.AddonArchiveTable +import Comp.Basic as B +import Comp.ItemDetail.Model exposing (Msg(..)) +import Comp.MenuBar as MB +import Data.Flags exposing (Flags) +import Data.ServerEvent exposing (AddonInfo) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Http +import Markdown +import Messages.Comp.AddonArchiveManage exposing (Texts) +import Page exposing (Page(..)) +import Styles as S + + +type FormError + = FormErrorNone + | FormErrorHttp Http.Error + | FormErrorInvalid + | FormErrorSubmit String + + +type ViewMode + = Table + | Form + + +type DeleteConfirm + = DeleteConfirmOff + | DeleteConfirmOn + + +type alias Model = + { viewMode : ViewMode + , addons : List Addon + , formModel : Comp.AddonArchiveForm.Model + , loading : Bool + , formError : FormError + , deleteConfirm : DeleteConfirm + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + let + ( fm, fc ) = + Comp.AddonArchiveForm.init + in + ( { viewMode = Table + , addons = [] + , formModel = fm + , loading = False + , formError = FormErrorNone + , deleteConfirm = DeleteConfirmOff + } + , Cmd.batch + [ Cmd.map FormMsg fc + , Api.addonsGetAll flags LoadAddonsResp + ] + ) + + +type Msg + = LoadAddons + | TableMsg Comp.AddonArchiveTable.Msg + | FormMsg Comp.AddonArchiveForm.Msg + | InitNewAddon + | SetViewMode ViewMode + | Submit + | RequestDelete + | CancelDelete + | DeleteAddonNow String + | LoadAddonsResp (Result Http.Error AddonList) + | AddAddonResp (Result Http.Error BasicResult) + | UpdateAddonResp (Result Http.Error BasicResult) + | DeleteAddonResp (Result Http.Error BasicResult) + | AddonInstallResp AddonInfo + + +loadAddons : Msg +loadAddons = + LoadAddons + + +addonInstallResult : AddonInfo -> Msg +addonInstallResult info = + AddonInstallResp info + + + +--- update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags msg model = + case msg of + InitNewAddon -> + let + ( bm, bc ) = + Comp.AddonArchiveForm.init + + nm = + { model + | viewMode = Form + , formError = FormErrorNone + , formModel = bm + } + in + ( nm, Cmd.map FormMsg bc, Sub.none ) + + SetViewMode vm -> + ( { model | viewMode = vm, formError = FormErrorNone } + , if vm == Table then + Api.addonsGetAll flags LoadAddonsResp + + else + Cmd.none + , Sub.none + ) + + FormMsg lm -> + let + ( fm, fc ) = + Comp.AddonArchiveForm.update flags lm model.formModel + in + ( { model | formModel = fm, formError = FormErrorNone } + , Cmd.map FormMsg fc + , Sub.none + ) + + TableMsg lm -> + let + action = + Comp.AddonArchiveTable.update lm + in + case action of + Comp.AddonArchiveTable.Selected addon -> + let + ( bm, bc ) = + Comp.AddonArchiveForm.initWith addon + in + ( { model + | viewMode = Form + , formError = FormErrorNone + , formModel = bm + } + , Cmd.map FormMsg bc + , Sub.none + ) + + RequestDelete -> + ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none, Sub.none ) + + CancelDelete -> + ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none, Sub.none ) + + DeleteAddonNow id -> + ( { model | deleteConfirm = DeleteConfirmOff, loading = True } + , Api.addonsDelete flags id DeleteAddonResp + , Sub.none + ) + + LoadAddons -> + ( { model | loading = True } + , Api.addonsGetAll flags LoadAddonsResp + , Sub.none + ) + + LoadAddonsResp (Ok list) -> + ( { model | loading = False, addons = list.items, formError = FormErrorNone } + , Cmd.none + , Sub.none + ) + + LoadAddonsResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) + + AddonInstallResp info -> + if info.success then + ( { model | loading = False, viewMode = Table }, Api.addonsGetAll flags LoadAddonsResp, Sub.none ) + + else + ( { model | loading = False, formError = FormErrorSubmit info.message }, Cmd.none, Sub.none ) + + Submit -> + case Comp.AddonArchiveForm.get model.formModel of + Just data -> + if data.id /= "" then + ( { model | loading = True } + , Api.addonsUpdate flags data.id UpdateAddonResp + , Sub.none + ) + + else + ( { model | loading = True } + , Api.addonsInstall + flags + (AddonRegister <| Maybe.withDefault "" data.url) + AddAddonResp + , Sub.none + ) + + Nothing -> + ( { model | formError = FormErrorInvalid }, Cmd.none, Sub.none ) + + AddAddonResp (Ok res) -> + if res.success then + ( model, Cmd.none, Sub.none ) + + else + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) + + AddAddonResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) + + UpdateAddonResp (Ok res) -> + if res.success then + ( model, Cmd.none, Sub.none ) + + else + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) + + UpdateAddonResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) + + DeleteAddonResp (Ok res) -> + if res.success then + update flags (SetViewMode Table) { model | loading = False } + + else + ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none, Sub.none ) + + DeleteAddonResp (Err err) -> + ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none, Sub.none ) + + + +--- view + + +view : Texts -> UiSettings -> Flags -> Model -> Html Msg +view texts settings flags model = + if model.viewMode == Table then + viewTable texts model + + else + viewForm texts settings flags model + + +viewTable : Texts -> Model -> Html Msg +viewTable texts model = + div [ class "flex flex-col" ] + [ MB.view + { start = + [] + , end = + [ MB.PrimaryButton + { tagger = InitNewAddon + , title = texts.createNewAddonArchive + , icon = Just "fa fa-plus" + , label = texts.newAddonArchive + } + ] + , rootClasses = "mb-4" + , sticky = True + } + , div + [ class "flex flex-col" + ] + [ Html.map TableMsg + (Comp.AddonArchiveTable.view texts.addonArchiveTable model.addons) + ] + , B.loadingDimmer + { label = "" + , active = model.loading + } + ] + + +viewForm : Texts -> UiSettings -> Flags -> Model -> Html Msg +viewForm texts _ _ model = + let + newAddon = + model.formModel.addon.id == "" + + isValid = + Comp.AddonArchiveForm.get model.formModel /= Nothing + in + div [ class "relative" ] + [ Html.form [] + [ if newAddon then + h1 [ class S.header2 ] + [ text texts.createNewAddonArchive + ] + + else + h1 [ class S.header2 ] + [ text (Comp.AddonArchiveForm.get model.formModel |> Maybe.map .name |> Maybe.withDefault "Update") + ] + , MB.view + { start = + [ MB.SecondaryButton + { tagger = SetViewMode Table + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.back + } + ] + , end = + if not newAddon then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisAddonArchive + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] + + else + [] + , rootClasses = "mb-4" + , sticky = True + } + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage + ] + [ case model.formError of + FormErrorNone -> + text "" + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.correctFormErrors + + FormErrorSubmit m -> + text m + ] + , div [] + [ Html.map FormMsg (Comp.AddonArchiveForm.view texts.addonArchiveForm model.formModel) + ] + , MB.view + { start = + [ MB.PrimaryButton + { tagger = Submit + , title = texts.installNow + , icon = + if newAddon then + Just "fa fa-save" + + else + Just "fa fa-arrows-rotate" + , label = + if newAddon then + texts.installNow + + else + texts.updateNow + } + ] + , end = [] + , rootClasses = "mb-4" + , sticky = False + } + , div + [ class "mb-4" + , classList [ ( "hidden", newAddon ) ] + ] + [ label [ class S.inputLabel ] [ text texts.description ] + , case model.formModel.addon.description of + Just desc -> + Markdown.toHtml [ class "markdown-preview" ] desc + + Nothing -> + div [ class "italic" ] [ text "-" ] + ] + , B.loadingDimmer + { active = model.loading + , label = texts.basics.loading + } + , B.contentDimmer + (model.deleteConfirm == DeleteConfirmOn) + (div [ class "flex flex-col" ] + [ div [ class "text-lg" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteAddonArchive + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteAddonNow model.formModel.addon.id) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/AddonArchiveTable.elm b/modules/webapp/src/main/elm/Comp/AddonArchiveTable.elm new file mode 100644 index 00000000..4b58cb81 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonArchiveTable.elm @@ -0,0 +1,72 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonArchiveTable exposing (..) + +import Api.Model.Addon exposing (Addon) +import Comp.Basic as B +import Html exposing (Html, div, table, tbody, td, text, th, thead, tr) +import Html.Attributes exposing (class) +import Messages.Comp.AddonArchiveTable exposing (Texts) +import Styles as S + + +type Msg + = SelectAddon Addon + + +type TableAction + = Selected Addon + + + +--- Update + + +update : Msg -> TableAction +update msg = + case msg of + SelectAddon addon -> + Selected addon + + + +--- View + + +view : Texts -> List Addon -> Html Msg +view texts addons = + table [ class S.tableMain ] + [ thead [] + [ tr [] + [ th [ class "" ] [] + , th [ class "text-left" ] + [ text texts.basics.name + ] + , th [ class "text-left" ] + [ text texts.version + ] + ] + ] + , tbody [] + (List.map (renderAddonLine texts) addons) + ] + + +renderAddonLine : Texts -> Addon -> Html Msg +renderAddonLine texts addon = + tr + [ class S.tableRow + ] + [ B.editLinkTableCell texts.basics.edit (SelectAddon addon) + , td [ class "text-left py-4 md:py-2" ] + [ text addon.name + ] + , td [ class "text-left" ] + [ text addon.version + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/AddonRunConfigForm.elm b/modules/webapp/src/main/elm/Comp/AddonRunConfigForm.elm new file mode 100644 index 00000000..0ad60d8f --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonRunConfigForm.elm @@ -0,0 +1,709 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonRunConfigForm exposing (Model, Msg, get, init, initWith, update, view) + +import Api +import Api.Model.Addon exposing (Addon) +import Api.Model.AddonList exposing (AddonList) +import Api.Model.AddonRef exposing (AddonRef) +import Api.Model.AddonRunConfig exposing (AddonRunConfig) +import Api.Model.User exposing (User) +import Api.Model.UserList exposing (UserList) +import Comp.Basic as B +import Comp.CalEventInput +import Comp.Dropdown +import Comp.MenuBar as MB +import Data.AddonTrigger exposing (AddonTrigger) +import Data.CalEvent exposing (CalEvent) +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Data.TimeZone exposing (TimeZone) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) +import Http +import Markdown +import Messages.Comp.AddonRunConfigForm exposing (Texts) +import Process +import Styles as S +import Task +import Util.List +import Util.String + + +type alias Model = + { runConfig : AddonRunConfig + , name : String + , enabled : Bool + , userDropdown : Comp.Dropdown.Model User + , userId : Maybe String + , userList : List User + , scheduleModel : Maybe Comp.CalEventInput.Model + , schedule : Maybe CalEvent + , triggerDropdown : Comp.Dropdown.Model AddonTrigger + , addons : List AddonRef + , selectedAddon : Maybe AddonConfigModel + , existingAddonDropdown : Comp.Dropdown.Model Addon + , existingAddons : List Addon + , configApplied : Bool + } + + +type alias AddonConfigModel = + { ref : AddonRef + , position : Int + , args : String + , readMore : Bool + } + + +getRef : AddonConfigModel -> AddonRef +getRef cfg = + let + a = + cfg.ref + in + { a | args = cfg.args } + + +emptyModel : Model +emptyModel = + { runConfig = Api.Model.AddonRunConfig.empty + , name = "" + , enabled = True + , userDropdown = Comp.Dropdown.makeSingle + , userId = Nothing + , userList = [] + , scheduleModel = Nothing + , schedule = Nothing + , triggerDropdown = + Comp.Dropdown.makeMultipleList + { options = Data.AddonTrigger.all, selected = [] } + , addons = [] + , selectedAddon = Nothing + , existingAddonDropdown = Comp.Dropdown.makeSingle + , existingAddons = [] + , configApplied = False + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel + , Cmd.batch + [ Api.getUsers flags UserListResp + , Api.addonsGetAll flags AddonListResp + ] + ) + + +initWith : Flags -> AddonRunConfig -> ( Model, Cmd Msg ) +initWith flags a = + let + ce = + Maybe.andThen Data.CalEvent.fromEvent a.schedule + + ceInit = + Maybe.map (Comp.CalEventInput.init flags) ce + + triggerModel = + Comp.Dropdown.makeMultipleList + { options = Data.AddonTrigger.all + , selected = Data.AddonTrigger.fromList a.trigger + } + in + ( { emptyModel + | runConfig = a + , name = a.name + , enabled = a.enabled + , scheduleModel = Maybe.map Tuple.first ceInit + , schedule = ce + , triggerDropdown = triggerModel + , userId = a.userId + , addons = a.addons + } + , Cmd.batch + [ Api.getUsers flags UserListResp + , Api.addonsGetAll flags AddonListResp + , Maybe.map Tuple.second ceInit + |> Maybe.map (Cmd.map ScheduleMsg) + |> Maybe.withDefault Cmd.none + ] + ) + + +isValid : Model -> Bool +isValid model = + model.name + /= "" + && (Comp.Dropdown.getSelected model.triggerDropdown + |> List.isEmpty + |> not + ) + && (List.isEmpty model.addons + |> not + ) + + +get : Model -> Maybe AddonRunConfig +get model = + let + a = + model.runConfig + in + if isValid model then + Just + { a + | name = model.name + , enabled = model.enabled + , schedule = Maybe.map Data.CalEvent.makeEvent model.schedule + , trigger = + Comp.Dropdown.getSelected model.triggerDropdown + |> List.map Data.AddonTrigger.asString + , userId = model.userId + , addons = model.addons + } + + else + Nothing + + +type Msg + = SetName String + | UserListResp (Result Http.Error UserList) + | AddonListResp (Result Http.Error AddonList) + | ScheduleMsg Comp.CalEventInput.Msg + | UserDropdownMsg (Comp.Dropdown.Msg User) + | TriggerDropdownMsg (Comp.Dropdown.Msg AddonTrigger) + | AddonDropdownMsg (Comp.Dropdown.Msg Addon) + | Configure Int AddonRef + | Up Int + | Down Int + | Remove Int + | ToggleEnabled + | ConfigSetArgs String + | ConfigApply + | ConfigCancel + | AddSelectedAddon + | ConfigToggleReadMore + | ConfigArgsUpdated Bool + + + +--- Update + + +update : Flags -> TimeZone -> Msg -> Model -> ( Model, Cmd Msg ) +update flags tz msg model = + case msg of + UserListResp (Ok list) -> + let + um = + Comp.Dropdown.makeSingleList + { options = list.items + , selected = Nothing + } + in + ( { model | userDropdown = um, userList = list.items }, Cmd.none ) + + UserListResp (Err err) -> + ( model, Cmd.none ) + + AddonListResp (Ok list) -> + let + am = + Comp.Dropdown.makeSingleList + { options = list.items + , selected = Nothing + } + in + ( { model | existingAddonDropdown = am, existingAddons = list.items }, Cmd.none ) + + AddonListResp (Err err) -> + ( model, Cmd.none ) + + UserDropdownMsg lm -> + let + ( um, cmd ) = + Comp.Dropdown.update lm model.userDropdown + + sel = + Comp.Dropdown.getSelected um |> List.head + in + ( { model | userDropdown = um, userId = Maybe.map .id sel }, Cmd.map UserDropdownMsg cmd ) + + TriggerDropdownMsg lm -> + let + ( tm, tc ) = + Comp.Dropdown.update lm model.triggerDropdown + + ( nm, nc ) = + initScheduleIfNeeded flags { model | triggerDropdown = tm } tz + in + ( nm, Cmd.batch [ Cmd.map TriggerDropdownMsg tc, nc ] ) + + ScheduleMsg lm -> + case model.scheduleModel of + Just m -> + let + ( cm, cc, ce ) = + Comp.CalEventInput.update flags tz model.schedule lm m + in + ( { model | scheduleModel = Just cm, schedule = ce }, Cmd.map ScheduleMsg cc ) + + Nothing -> + ( model, Cmd.none ) + + ToggleEnabled -> + ( { model | enabled = not model.enabled }, Cmd.none ) + + AddonDropdownMsg lm -> + let + ( am, ac ) = + Comp.Dropdown.update lm model.existingAddonDropdown + in + ( { model | existingAddonDropdown = am }, Cmd.map AddonDropdownMsg ac ) + + Configure index ref -> + let + cfg = + { ref = ref + , position = index + 1 + , args = ref.args + , readMore = False + } + in + ( { model | selectedAddon = Just cfg }, Cmd.none ) + + ConfigCancel -> + ( { model | selectedAddon = Nothing }, Cmd.none ) + + ConfigToggleReadMore -> + case model.selectedAddon of + Just cfg -> + ( { model | selectedAddon = Just { cfg | readMore = not cfg.readMore } }, Cmd.none ) + + Nothing -> + ( model, Cmd.none ) + + ConfigArgsUpdated flag -> + ( { model | configApplied = flag }, Cmd.none ) + + ConfigSetArgs str -> + case model.selectedAddon of + Just cfg -> + ( { model | selectedAddon = Just { cfg | args = str } } + , Cmd.none + ) + + Nothing -> + ( model, Cmd.none ) + + ConfigApply -> + case model.selectedAddon of + Just cfg -> + let + na = + getRef cfg + + addons = + Util.List.replaceByIndex (cfg.position - 1) na model.addons + in + ( { model | addons = addons, configApplied = True } + , Process.sleep 1200 |> Task.perform (\_ -> ConfigArgsUpdated False) + ) + + Nothing -> + ( model, Cmd.none ) + + AddSelectedAddon -> + let + sel = + Comp.Dropdown.getSelected model.existingAddonDropdown |> List.head + + ( dm, _ ) = + Comp.Dropdown.update (Comp.Dropdown.SetSelection []) model.existingAddonDropdown + + addon = + Maybe.map + (\a -> + { addonId = a.id + , name = a.name + , version = a.version + , description = a.description + , args = "" + } + ) + sel + + newAddons = + Maybe.map (\e -> e :: model.addons) addon + |> Maybe.withDefault model.addons + in + ( { model | addons = newAddons, existingAddonDropdown = dm, selectedAddon = Nothing }, Cmd.none ) + + Up curIndex -> + let + newAddons = + Util.List.changePosition curIndex (curIndex - 1) model.addons + in + ( { model | addons = newAddons, selectedAddon = Nothing }, Cmd.none ) + + Down curIndex -> + let + newAddons = + Util.List.changePosition (curIndex + 1) curIndex model.addons + in + ( { model | addons = newAddons, selectedAddon = Nothing }, Cmd.none ) + + SetName str -> + ( { model | name = str }, Cmd.none ) + + Remove index -> + ( { model | addons = Util.List.removeByIndex index model.addons, selectedAddon = Nothing }, Cmd.none ) + + +initScheduleIfNeeded : Flags -> Model -> TimeZone -> ( Model, Cmd Msg ) +initScheduleIfNeeded flags model tz = + let + hasTrigger = + Comp.Dropdown.getSelected model.triggerDropdown + |> List.any ((==) Data.AddonTrigger.Scheduled) + + noModel = + model.scheduleModel == Nothing + + hasModel = + not noModel + + ce = + Data.CalEvent.everyMonthTz tz + + ( cm, cc ) = + Comp.CalEventInput.init flags ce + in + if hasTrigger && noModel then + ( { model | scheduleModel = Just cm, schedule = Just ce }, Cmd.map ScheduleMsg cc ) + + else if not hasTrigger && hasModel then + ( { model | scheduleModel = Nothing, schedule = Nothing }, Cmd.none ) + + else + ( model, Cmd.none ) + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + userDs = + { makeOption = \user -> { text = user.login, additional = "" } + , placeholder = texts.basics.selectPlaceholder + , labelColor = \_ -> \_ -> "" + , style = DS.mainStyle + } + + triggerDs = + { makeOption = \trigger -> { text = Data.AddonTrigger.asString trigger, additional = "" } + , placeholder = texts.basics.selectPlaceholder + , labelColor = \_ -> \_ -> "" + , style = DS.mainStyle + } + in + div + [ class "flex flex-col" ] + [ div [ class "mb-4" ] + [ div [ class "mb-4" ] + [ label + [ class S.inputLabel + ] + [ text texts.basics.name + , B.inputRequired + ] + , input + [ type_ "text" + , placeholder texts.chooseName + , value model.name + , onInput SetName + , class S.textInput + , classList [ ( S.inputErrorBorder, model.name == "" ) ] + ] + [] + ] + , div [ class "mb-4" ] + [ MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleEnabled + , label = texts.enableDisable + , value = model.enabled + , id = "addon-run-config-enabled" + } + ] + , div [ class "mb-4" ] + [ label + [ class S.inputLabel + ] + [ text texts.impersonateUser + ] + , Html.map UserDropdownMsg + (Comp.Dropdown.view2 userDs settings model.userDropdown) + ] + , div [ class "mb-4" ] + [ label + [ class S.inputLabel + ] + [ text texts.triggerRun + , B.inputRequired + ] + , Html.map TriggerDropdownMsg + (Comp.Dropdown.view2 triggerDs settings model.triggerDropdown) + ] + , case model.scheduleModel of + Nothing -> + span [ class "hidden" ] [] + + Just m -> + div [ class "mb-4" ] + [ label + [ class S.inputLabel ] + [ text texts.schedule + ] + , Html.map ScheduleMsg (Comp.CalEventInput.view2 texts.calEventInput "" model.schedule m) + ] + ] + , div [ class "mb-4" ] + [ h2 [ class S.header2 ] + [ text texts.addons ] + , addonRef texts model + , div [ class "mb-4" ] + [ label [ class S.inputLabel ] + [ text texts.includedAddons + , B.inputRequired + ] + , newAddon texts settings model + , div [ class "mb-4" ] + [ div [ class "flex flex-col mb-4" ] + (List.indexedMap (addonLine texts model) model.addons) + ] + ] + ] + ] + + +newAddon : Texts -> UiSettings -> Model -> Html Msg +newAddon texts uiSettings model = + let + addonDs = + { makeOption = \addon -> { text = addon.name ++ " / " ++ addon.version, additional = "" } + , placeholder = texts.basics.selectPlaceholder + , labelColor = \_ -> \_ -> "" + , style = DS.mainStyle + } + in + div [ class "mb-4" ] + [ div [ class "flex flex-row" ] + [ div [ class "flex-grow mr-2" ] + [ Html.map AddonDropdownMsg + (Comp.Dropdown.view2 addonDs uiSettings model.existingAddonDropdown) + ] + , B.primaryBasicButton + { label = texts.add + , icon = "fa fa-plus" + , disabled = List.isEmpty (Comp.Dropdown.getSelected model.existingAddonDropdown) + , handler = onClick AddSelectedAddon + , attrs = [ href "#" ] + } + ] + ] + + +addonRef : Texts -> Model -> Html Msg +addonRef texts model = + let + maybeRef = + Maybe.map .ref model.selectedAddon + + refInfo = + case model.selectedAddon of + Nothing -> + div [ class "mb-4" ] + [ text "[ -- ]" + ] + + Just cfg -> + let + ( descr, requireFolding ) = + case cfg.ref.description of + Just d -> + let + part = + Util.String.firstSentenceOrMax 120 d + + text = + if cfg.readMore then + d + + else + Maybe.withDefault d part + in + ( Markdown.toHtml [ class "markdown-preview" ] text, part /= Nothing ) + + Nothing -> + ( span [ class "italic" ] [ text "No description." ], False ) + in + div [ class "flex flex-col mb-4" ] + [ div [ class "mt-2" ] + [ label [ class " font-semibold py-0.5 " ] + [ text cfg.ref.name + , text " " + , text cfg.ref.version + , text " (pos. " + , text <| String.fromInt cfg.position + , text ")" + , span + [ classList [ ( "hidden", not requireFolding ) ] + , class "ml-2" + ] + [ a + [ class "px-4" + , class S.link + , href "#" + , onClick ConfigToggleReadMore + ] + [ if cfg.readMore then + text texts.readLess + + else + text texts.readMore + ] + ] + ] + , div [ class "px-3 py-1 border-l dark:border-slate-600" ] + [ descr + ] + ] + ] + in + div + [ class "flex flex-col mb-3" + , classList [ ( "disabled", maybeRef == Nothing ) ] + ] + [ refInfo + , div [ class "mb-2" ] + [ label [ class S.inputLabel ] [ text texts.arguments ] + , textarea + [ Maybe.map .args model.selectedAddon |> Maybe.withDefault "" |> value + , class S.textAreaInput + , class "font-mono" + , rows 8 + , onInput ConfigSetArgs + ] + [] + ] + , MB.view + { start = + [ MB.PrimaryButton + { tagger = ConfigApply + , title = "" + , icon = Just "fa fa-save" + , label = texts.update + } + , MB.SecondaryButton + { tagger = ConfigCancel + , title = texts.basics.cancel + , icon = Just "fa fa-times" + , label = texts.basics.cancel + } + , MB.CustomElement <| + div + [ classList [ ( "hidden", not model.configApplied ) ] + , class S.successText + , class "inline-block min-w-fit font-semibold text-normal min-w-fit" + ] + [ text texts.argumentsUpdated + , i [ class "fa fa-thumbs-up ml-2" ] [] + ] + ] + , end = [] + , rootClasses = "mb-4 text-sm" + , sticky = False + } + ] + + +addonLine : Texts -> Model -> Int -> AddonRef -> Html Msg +addonLine texts model index ref = + let + isSelected = + case model.selectedAddon of + Just cfg -> + cfg.position - 1 == index + + Nothing -> + False + in + div + [ class "flex flex-row items-center px-4 py-4 rounded shadow dark:border dark:border-slate-600 mb-2" + , classList [ ( "ring-2", isSelected ) ] + ] + [ div [ class "px-2 hidden sm:block" ] + [ span [ class "label rounded-full opacity-75" ] + [ text <| String.fromInt (index + 1) + ] + ] + , div [ class "px-4 font-semibold" ] + [ text ref.name + , text " v" + , text ref.version + ] + , div [ class "flex-grow" ] + [] + , div [ class "px-2" ] + [ MB.view + { start = [] + , end = + [ MB.PrimaryButton + { tagger = Configure index ref + , title = texts.configureTitle + , icon = Just "fa fa-cog" + , label = texts.configureLabel + } + , MB.CustomElement <| + B.secondaryButton + { handler = onClick (Up index) + , attrs = [ title "Move up", href "#" ] + , icon = "fa fa-arrow-up" + , label = "" + , disabled = index == 0 + } + , MB.CustomElement <| + B.secondaryButton + { handler = onClick (Down index) + , attrs = [ title "Move down", href "#" ] + , icon = "fa fa-arrow-down" + , label = "" + , disabled = index + 1 == List.length model.addons + } + , MB.CustomElement <| + B.deleteButton + { label = "" + , icon = "fa fa-trash" + , disabled = False + , handler = onClick (Remove index) + , attrs = [ href "#" ] + } + ] + , rootClasses = "text-sm" + , sticky = False + } + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/AddonRunConfigManage.elm b/modules/webapp/src/main/elm/Comp/AddonRunConfigManage.elm new file mode 100644 index 00000000..641b821d --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonRunConfigManage.elm @@ -0,0 +1,364 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonRunConfigManage exposing (Model, Msg, init, loadConfigs, update, view) + +import Api +import Api.Model.AddonRunConfig exposing (AddonRunConfig) +import Api.Model.AddonRunConfigList exposing (AddonRunConfigList) +import Api.Model.BasicResult exposing (BasicResult) +import Comp.AddonRunConfigForm +import Comp.AddonRunConfigTable +import Comp.Basic as B +import Comp.ItemDetail.Model exposing (Msg(..)) +import Comp.MenuBar as MB +import Data.Flags exposing (Flags) +import Data.TimeZone exposing (TimeZone) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Http +import Messages.Comp.AddonRunConfigManage exposing (Texts) +import Page exposing (Page(..)) +import Styles as S + + +type FormError + = FormErrorNone + | FormErrorHttp Http.Error + | FormErrorInvalid + | FormErrorSubmit String + + +type ViewMode + = Table + | Form + + +type DeleteConfirm + = DeleteConfirmOff + | DeleteConfirmOn + + +type alias Model = + { viewMode : ViewMode + , runConfigs : List AddonRunConfig + , formModel : Comp.AddonRunConfigForm.Model + , loading : Bool + , formError : FormError + , deleteConfirm : DeleteConfirm + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + let + ( fm, fc ) = + Comp.AddonRunConfigForm.init flags + in + ( { viewMode = Table + , runConfigs = [] + , formModel = fm + , loading = False + , formError = FormErrorNone + , deleteConfirm = DeleteConfirmOff + } + , Cmd.batch + [ Cmd.map FormMsg fc + , Api.addonRunConfigGet flags LoadConfigsResp + ] + ) + + +type Msg + = LoadRunConfigs + | TableMsg Comp.AddonRunConfigTable.Msg + | FormMsg Comp.AddonRunConfigForm.Msg + | InitNewConfig + | SetViewMode ViewMode + | Submit + | RequestDelete + | CancelDelete + | DeleteConfigNow String + | LoadConfigsResp (Result Http.Error AddonRunConfigList) + | AddConfigResp (Result Http.Error BasicResult) + | DeleteConfigResp (Result Http.Error BasicResult) + + +loadConfigs : Msg +loadConfigs = + LoadRunConfigs + + + +--- update + + +update : Flags -> TimeZone -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags tz msg model = + case msg of + InitNewConfig -> + let + ( bm, bc ) = + Comp.AddonRunConfigForm.init flags + + nm = + { model + | viewMode = Form + , formError = FormErrorNone + , formModel = bm + } + in + ( nm, Cmd.map FormMsg bc, Sub.none ) + + SetViewMode vm -> + ( { model | viewMode = vm, formError = FormErrorNone } + , if vm == Table then + Api.addonRunConfigGet flags LoadConfigsResp + + else + Cmd.none + , Sub.none + ) + + FormMsg lm -> + let + ( fm, fc ) = + Comp.AddonRunConfigForm.update flags tz lm model.formModel + in + ( { model | formModel = fm, formError = FormErrorNone } + , Cmd.map FormMsg fc + , Sub.none + ) + + TableMsg lm -> + let + action = + Comp.AddonRunConfigTable.update lm + in + case action of + Comp.AddonRunConfigTable.Selected addon -> + let + ( bm, bc ) = + Comp.AddonRunConfigForm.initWith flags addon + in + ( { model + | viewMode = Form + , formError = FormErrorNone + , formModel = bm + } + , Cmd.map FormMsg bc + , Sub.none + ) + + RequestDelete -> + ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none, Sub.none ) + + CancelDelete -> + ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none, Sub.none ) + + DeleteConfigNow id -> + ( { model | deleteConfirm = DeleteConfirmOff, loading = True } + , Api.addonRunConfigDelete flags id DeleteConfigResp + , Sub.none + ) + + LoadRunConfigs -> + ( { model | loading = True } + , Api.addonRunConfigGet flags LoadConfigsResp + , Sub.none + ) + + LoadConfigsResp (Ok list) -> + ( { model | loading = False, runConfigs = list.items, formError = FormErrorNone } + , Cmd.none + , Sub.none + ) + + LoadConfigsResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) + + Submit -> + case Comp.AddonRunConfigForm.get model.formModel of + Just data -> + ( { model | loading = True }, Api.addonRunConfigSet flags data AddConfigResp, Sub.none ) + + Nothing -> + ( { model | formError = FormErrorInvalid }, Cmd.none, Sub.none ) + + AddConfigResp (Ok res) -> + if res.success then + ( { model | loading = False }, Cmd.none, Sub.none ) + + else + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) + + AddConfigResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) + + DeleteConfigResp (Ok res) -> + if res.success then + update flags tz (SetViewMode Table) { model | loading = False } + + else + ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none, Sub.none ) + + DeleteConfigResp (Err err) -> + ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none, Sub.none ) + + + +--- view + + +view : Texts -> UiSettings -> Flags -> Model -> Html Msg +view texts settings flags model = + if model.viewMode == Table then + viewTable texts model + + else + viewForm texts settings flags model + + +viewTable : Texts -> Model -> Html Msg +viewTable texts model = + div [ class "flex flex-col" ] + [ MB.view + { start = + [] + , end = + [ MB.PrimaryButton + { tagger = InitNewConfig + , title = texts.createNewAddonRunConfig + , icon = Just "fa fa-plus" + , label = texts.newAddonRunConfig + } + ] + , rootClasses = "mb-4" + , sticky = True + } + , div + [ class "flex flex-col" + ] + [ Html.map TableMsg + (Comp.AddonRunConfigTable.view texts.addonArchiveTable model.runConfigs) + ] + , B.loadingDimmer + { label = "" + , active = model.loading + } + ] + + +viewForm : Texts -> UiSettings -> Flags -> Model -> Html Msg +viewForm texts uiSettings _ model = + let + newConfig = + model.formModel.runConfig.id == "" + + isValid = + Comp.AddonRunConfigForm.get model.formModel /= Nothing + in + div [] + [ Html.form [] + [ if newConfig then + h1 [ class S.header2 ] + [ text texts.createNewAddonRunConfig + ] + + else + h1 [ class S.header2 ] + [ text (Comp.AddonRunConfigForm.get model.formModel |> Maybe.map .name |> Maybe.withDefault "Update") + ] + , MB.view + { start = + [ MB.CustomElement <| + B.primaryButton + { handler = onClick Submit + , title = texts.basics.submitThisForm + , icon = "fa fa-save" + , label = texts.basics.submit + , disabled = not isValid + , attrs = [ href "#" ] + } + , MB.SecondaryButton + { tagger = SetViewMode Table + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.back + } + ] + , end = + if not newConfig then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisAddonRunConfig + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] + + else + [] + , rootClasses = "mb-4" + , sticky = True + } + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage + ] + [ case model.formError of + FormErrorNone -> + text "" + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.correctFormErrors + + FormErrorSubmit m -> + text m + ] + , div [] + [ Html.map FormMsg (Comp.AddonRunConfigForm.view texts.addonArchiveForm uiSettings model.formModel) + ] + , B.loadingDimmer + { active = model.loading + , label = texts.basics.loading + } + , B.contentDimmer + (model.deleteConfirm == DeleteConfirmOn) + (div [ class "flex flex-col" ] + [ div [ class "text-lg" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteAddonRunConfig + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteConfigNow model.formModel.runConfig.id) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/AddonRunConfigTable.elm b/modules/webapp/src/main/elm/Comp/AddonRunConfigTable.elm new file mode 100644 index 00000000..02febe38 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/AddonRunConfigTable.elm @@ -0,0 +1,79 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.AddonRunConfigTable exposing (..) + +import Api.Model.AddonRunConfig exposing (AddonRunConfig) +import Comp.Basic as B +import Html exposing (Html, div, table, tbody, td, text, th, thead, tr) +import Html.Attributes exposing (class) +import Messages.Comp.AddonRunConfigTable exposing (Texts) +import Styles as S +import Util.Html + + +type Msg + = SelectRunConfig AddonRunConfig + + +type TableAction + = Selected AddonRunConfig + + + +--- Update + + +update : Msg -> TableAction +update msg = + case msg of + SelectRunConfig cfg -> + Selected cfg + + + +--- View + + +view : Texts -> List AddonRunConfig -> Html Msg +view texts addons = + table [ class S.tableMain ] + [ thead [] + [ tr [] + [ th [ class "" ] [] + , th [ class "text-left" ] + [ text texts.basics.name + ] + , th [ class "px-2 text-center" ] [ text texts.enabled ] + , th [ class "px-2 text-left" ] [ text texts.trigger ] + , th [ class "px-2 text-center" ] [ text "# Addons" ] + ] + ] + , tbody [] + (List.map (renderRunConfigLine texts) addons) + ] + + +renderRunConfigLine : Texts -> AddonRunConfig -> Html Msg +renderRunConfigLine texts cfg = + tr + [ class S.tableRow + ] + [ B.editLinkTableCell texts.basics.edit (SelectRunConfig cfg) + , td [ class "text-left py-4 md:py-2" ] + [ text cfg.name + ] + , td [ class "w-px whitespace-nowrap px-2 text-center" ] + [ Util.Html.checkbox2 cfg.enabled + ] + , td [ class "px-2 text-left" ] + [ text (String.join ", " cfg.trigger) + ] + , td [ class "px-2 text-center" ] + [ text (String.fromInt <| List.length cfg.addons) + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index 726166fb..e01db9d7 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -28,6 +28,8 @@ module Comp.ItemDetail.Model exposing , resultModelCmdSub ) +import Api.Model.AddonRunConfig exposing (AddonRunConfig) +import Api.Model.AddonRunConfigList exposing (AddonRunConfigList) import Api.Model.BasicResult exposing (BasicResult) import Api.Model.CustomField exposing (CustomField) import Api.Model.EquipmentList exposing (EquipmentList) @@ -72,6 +74,7 @@ import Set exposing (Set) type alias Model = { item : ItemDetail + , runConfigs : List AddonRunConfig , visibleAttach : Int , attachMenuOpen : Bool , menuOpen : Bool @@ -123,6 +126,9 @@ type alias Model = , viewMode : ViewMode , showQrModel : ShowQrModel , itemLinkModel : Comp.ItemLinkForm.Model + , showRunAddon : Bool + , addonRunConfigDropdown : Comp.Dropdown.Model AddonRunConfig + , addonRunSubmitted : Bool } @@ -204,6 +210,7 @@ isEditNotes field = emptyModel : Model emptyModel = { item = Api.Model.ItemDetail.empty + , runConfigs = [] , visibleAttach = 0 , attachMenuOpen = False , menuOpen = False @@ -259,6 +266,9 @@ emptyModel = , viewMode = SimpleView , showQrModel = initShowQrModel , itemLinkModel = Comp.ItemLinkForm.emptyModel + , showRunAddon = False + , addonRunConfigDropdown = Comp.Dropdown.makeSingle + , addonRunSubmitted = False } @@ -373,6 +383,12 @@ type Msg | SetNameMsg Comp.SimpleTextInput.Msg | ToggleSelectItem | ItemLinkFormMsg Comp.ItemLinkForm.Msg + | ToggleShowRunAddon + | LoadRunConfigResp (Result Http.Error AddonRunConfigList) + | RunAddonMsg (Comp.Dropdown.Msg AddonRunConfig) + | RunSelectedAddon + | RunAddonResp (Result Http.Error BasicResult) + | SetAddonRunSubmitted Bool type SaveNameState diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/RunAddonForm.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/RunAddonForm.elm new file mode 100644 index 00000000..80705c5e --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/RunAddonForm.elm @@ -0,0 +1,78 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ItemDetail.RunAddonForm exposing (..) + +import Comp.Basic as B +import Comp.Dropdown +import Comp.ItemDetail.Model exposing (..) +import Comp.MenuBar as MB +import Data.DropdownStyle as DS +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, h3, label, text) +import Html.Attributes exposing (class, classList, title) +import Html.Events exposing (onClick) +import Messages.Comp.ItemDetail.RunAddonForm exposing (Texts) +import Styles as S + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts uiSettings model = + let + viewSettings = + { makeOption = \cfg -> { text = cfg.name, additional = "" } + , placeholder = texts.basics.selectPlaceholder + , labelColor = \_ -> \_ -> "" + , style = DS.mainStyle + } + + runDisabled = + Comp.Dropdown.getSelected model.addonRunConfigDropdown + |> List.isEmpty + in + div + [ classList [ ( "hidden", not model.showRunAddon ) ] + , class "mb-4" + ] + [ h3 [ class S.header3 ] [ text texts.runAddon ] + , div [ class "my-2" ] + [ label [ class S.inputLabel ] + [ text texts.addonRunConfig + ] + , Html.map RunAddonMsg (Comp.Dropdown.view2 viewSettings uiSettings model.addonRunConfigDropdown) + ] + , div [ class "my-2" ] + [ MB.view + { start = + [ MB.CustomElement <| + B.primaryButton + { label = "Run" + , icon = + if model.addonRunSubmitted then + "fa fa-check" + + else + "fa fa-play" + , disabled = runDisabled + , handler = onClick RunSelectedAddon + , attrs = + [ title texts.runAddonTitle + ] + } + , MB.SecondaryButton + { label = texts.basics.cancel + , icon = Just "fa fa-times" + , tagger = ToggleShowRunAddon + , title = "" + } + ] + , end = [] + , rootClasses = "text-sm mt-1" + , sticky = False + } + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index abc9db46..48871ecb 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -56,6 +56,7 @@ import Comp.PersonForm import Comp.SentMails import Comp.SimpleTextInput import Comp.TagDropdown +import Data.AddonTrigger import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.Direction import Data.Environment as Env @@ -75,7 +76,9 @@ import Html5.DragDrop as DD import Http import Page exposing (Page(..)) import Ports +import Process import Set exposing (Set) +import Task import Util.File exposing (makeFileId) import Util.List import Util.Maybe @@ -121,9 +124,77 @@ update inav env msg model = , Cmd.map ItemMailMsg ic , Cmd.map CustomFieldMsg cc , Api.getSentMails env.flags model.item.id SentMailsResp + , Api.addonRunConfigGet env.flags LoadRunConfigResp ] ) + LoadRunConfigResp (Ok list) -> + let + existingItem cfg = + cfg.enabled + && (Data.AddonTrigger.fromList cfg.trigger + |> List.any ((==) Data.AddonTrigger.ExistingItem) + ) + + configs = + List.filter existingItem list.items + + dropdown = + Comp.Dropdown.makeSingleList { options = configs, selected = Nothing } + in + resultModel { model | runConfigs = configs, addonRunConfigDropdown = dropdown } + + RunAddonMsg lm -> + let + ( dd, dc ) = + Comp.Dropdown.update lm model.addonRunConfigDropdown + in + resultModelCmd ( { model | addonRunConfigDropdown = dd }, Cmd.map RunAddonMsg dc ) + + RunSelectedAddon -> + let + configs = + Comp.Dropdown.getSelected model.addonRunConfigDropdown + |> List.map .id + + payload = + { itemId = model.item.id + , additionalItems = [] + , addonRunConfigIds = configs + } + + ( dd, _ ) = + Comp.Dropdown.update (Comp.Dropdown.SetSelection []) model.addonRunConfigDropdown + in + case configs of + [] -> + resultModel model + + _ -> + resultModelCmd + ( { model | addonRunConfigDropdown = dd } + , Api.addonRunExistingItem env.flags payload RunAddonResp + ) + + LoadRunConfigResp (Err _) -> + resultModel model + + RunAddonResp (Ok res) -> + if res.success then + resultModelCmd + ( { model | addonRunSubmitted = True } + , Process.sleep 1200 |> Task.perform (\_ -> SetAddonRunSubmitted False) + ) + + else + resultModel model + + RunAddonResp (Err _) -> + resultModel model + + SetAddonRunSubmitted flag -> + resultModel { model | addonRunSubmitted = flag } + SetItem item -> let res1 = @@ -1638,6 +1709,9 @@ update inav env msg model = , Sub.map ItemLinkFormMsg ils ) + ToggleShowRunAddon -> + resultModel { model | showRunAddon = not model.showRunAddon, mobileItemMenuOpen = False } + --- Helper diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm index 54a1d3d9..41abc845 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm @@ -22,6 +22,7 @@ import Comp.ItemDetail.Model , isShowQrItem ) import Comp.ItemDetail.Notes +import Comp.ItemDetail.RunAddonForm import Comp.ItemDetail.ShowQrCode import Comp.ItemDetail.SingleAttachment import Comp.ItemLinkForm @@ -177,6 +178,24 @@ menuBar texts inav env model = ] [ Icons.addFilesIcon2 "" ] + , MB.CustomElement <| + a + [ classList + [ ( "bg-gray-200 dark:bg-slate-600", model.showRunAddon ) + , ( "hidden", not env.flags.config.addonsEnabled || List.isEmpty model.runConfigs ) + , ( "hidden md:block", env.flags.config.addonsEnabled && not (List.isEmpty model.runConfigs) ) + ] + , if model.showRunAddon then + title texts.close + + else + title texts.runAddonTitle + , onClick ToggleShowRunAddon + , class S.secondaryBasicButton + , href "#" + ] + [ Icons.addonIcon "" + ] , MB.CustomElement <| a [ classList @@ -248,6 +267,15 @@ menuBar texts inav env model = , onClick AddFilesToggle ] } + , { icon = Icons.addonIcon "" + , label = texts.runAddonLabel + , disabled = False + , attrs = + [ href "#" + , onClick ToggleShowRunAddon + , classList [ ( "hidden", not env.flags.config.addonsEnabled ) ] + ] + } , { icon = Icons.showQrIcon "" , label = texts.showQrCode , disabled = False @@ -402,6 +430,11 @@ itemActions texts flags settings model classes = (S.border ++ " mb-4") model (Comp.ItemDetail.ShowQrCode.Item model.item.id) + , if flags.config.addonsEnabled then + Comp.ItemDetail.RunAddonForm.view texts.runAddonForm settings model + + else + span [ class "hidden" ] [] ] diff --git a/modules/webapp/src/main/elm/Data/AddonTrigger.elm b/modules/webapp/src/main/elm/Data/AddonTrigger.elm new file mode 100644 index 00000000..f790d4e0 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/AddonTrigger.elm @@ -0,0 +1,59 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.AddonTrigger exposing (..) + +-- A copy of docspell.addons.AddonTrigger.scala + + +type AddonTrigger + = FinalProcessItem + | FinalReprocessItem + | Scheduled + | ExistingItem + + +all : List AddonTrigger +all = + [ FinalProcessItem + , FinalReprocessItem + , Scheduled + , ExistingItem + ] + + +asString : AddonTrigger -> String +asString t = + case t of + FinalProcessItem -> + "final-process-item" + + FinalReprocessItem -> + "final-reprocess-item" + + Scheduled -> + "scheduled" + + ExistingItem -> + "existing-item" + + +fromString : String -> Maybe AddonTrigger +fromString s = + let + name = + String.toLower s + + x = + List.filter (\e -> asString e == name) all + in + List.head x + + +fromList : List String -> List AddonTrigger +fromList list = + List.filterMap fromString list diff --git a/modules/webapp/src/main/elm/Data/CalEvent.elm b/modules/webapp/src/main/elm/Data/CalEvent.elm index 6453f703..cff62da9 100644 --- a/modules/webapp/src/main/elm/Data/CalEvent.elm +++ b/modules/webapp/src/main/elm/Data/CalEvent.elm @@ -8,6 +8,7 @@ module Data.CalEvent exposing ( CalEvent , everyMonth + , everyMonthTz , fromEvent , makeEvent ) @@ -29,7 +30,12 @@ type alias CalEvent = everyMonth : CalEvent everyMonth = - CalEvent Nothing "*" "*" "01" "00" "00" Data.TimeZone.utc + everyMonthTz Data.TimeZone.utc + + +everyMonthTz : TimeZone -> CalEvent +everyMonthTz tz = + CalEvent Nothing "*" "*" "01" "00" "00" tz makeEvent : CalEvent -> String diff --git a/modules/webapp/src/main/elm/Data/Flags.elm b/modules/webapp/src/main/elm/Data/Flags.elm index f91aba31..614b452a 100644 --- a/modules/webapp/src/main/elm/Data/Flags.elm +++ b/modules/webapp/src/main/elm/Data/Flags.elm @@ -38,6 +38,7 @@ type alias Config = , downloadAllMaxFiles : Int , downloadAllMaxSize : Int , openIdAuth : List OpenIdAuth + , addonsEnabled : Bool } diff --git a/modules/webapp/src/main/elm/Data/Icons.elm b/modules/webapp/src/main/elm/Data/Icons.elm index 236390a7..c50cebb5 100644 --- a/modules/webapp/src/main/elm/Data/Icons.elm +++ b/modules/webapp/src/main/elm/Data/Icons.elm @@ -8,6 +8,8 @@ module Data.Icons exposing ( addFiles2 , addFilesIcon2 + , addonIcon + , addonRunConfigIcon , concerned , concerned2 , concernedIcon @@ -76,7 +78,7 @@ module Data.Icons exposing ) import Data.CustomFieldType exposing (CustomFieldType) -import Html exposing (Html, i, img) +import Html exposing (Html, div, i, img, span) import Html.Attributes exposing (class, src) import Svg import Svg.Attributes as SA @@ -265,6 +267,24 @@ customFieldIcon2 classes = i [ class (customField2 ++ " " ++ classes) ] [] +addon : String +addon = + "fa fa-puzzle-piece" + + +addonIcon : String -> Html msg +addonIcon classes = + i [ class (addon ++ " " ++ classes) ] [] + + +addonRunConfigIcon : String -> Html msg +addonRunConfigIcon classes = + div [ class (classes ++ " inline-block relative margin-auto leading-8") ] + [ i [ class "fa fa-puzzle-piece" ] [] + , i [ class "fa fa-play font-bold absolute text-xs -right-2 top-0" ] [] + ] + + search : String search = "fa fa-search" diff --git a/modules/webapp/src/main/elm/Data/ServerEvent.elm b/modules/webapp/src/main/elm/Data/ServerEvent.elm index 194ba0cc..57320996 100644 --- a/modules/webapp/src/main/elm/Data/ServerEvent.elm +++ b/modules/webapp/src/main/elm/Data/ServerEvent.elm @@ -5,15 +5,34 @@ -} -module Data.ServerEvent exposing (ServerEvent(..), decode) +module Data.ServerEvent exposing (AddonInfo, ServerEvent(..), decode) import Json.Decode as D +import Json.Decode.Pipeline as P type ServerEvent = JobSubmitted String | JobDone String | JobsWaiting Int + | AddonInstalled AddonInfo + + +type alias AddonInfo = + { success : Bool + , addonId : Maybe String + , addonUrl : Maybe String + , message : String + } + + +addonInfoDecoder : D.Decoder AddonInfo +addonInfoDecoder = + D.succeed AddonInfo + |> P.required "success" D.bool + |> P.optional "addonId" (D.maybe D.string) Nothing + |> P.optional "addonUrl" (D.maybe D.string) Nothing + |> P.required "message" D.string decoder : D.Decoder ServerEvent @@ -43,5 +62,9 @@ decodeTag tag = D.field "content" D.int |> D.map JobsWaiting + "addon-installed" -> + D.field "content" addonInfoDecoder + |> D.map AddonInstalled + _ -> D.fail ("Unknown tag: " ++ tag) diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveForm.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveForm.elm new file mode 100644 index 00000000..3bac22c4 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveForm.elm @@ -0,0 +1,54 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonArchiveForm exposing + ( Texts + , de + , fr + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , addonUrl : String + , addonUrlPlaceholder : String + , installInfoText : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , addonUrl = "Addon URL" + , addonUrlPlaceholder = "e.g. https://github.com/some-user/project/refs/tags/1.0.zip" + , installInfoText = "Only urls to remote addon zip files are supported." + } + + +de : Texts +de = + { basics = Messages.Basics.de + , addonUrl = "Addon URL" + , addonUrlPlaceholder = "z.B. https://github.com/some-user/project/refs/tags/1.0.zip" + , installInfoText = "Nur URLs to externen zip Dateien werden unterstützt." + } + + + +-- TODO: translate-fr + + +fr : Texts +fr = + { basics = Messages.Basics.fr + , addonUrl = "Addon URL" + , addonUrlPlaceholder = "p.e. https://github.com/some-user/project/refs/tags/1.0.zip" + , installInfoText = "Only urls to remote addon zip files are supported." + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveManage.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveManage.elm new file mode 100644 index 00000000..5cc6b135 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveManage.elm @@ -0,0 +1,86 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonArchiveManage exposing + ( Texts + , de + , fr + , gb + ) + +import Http +import Messages.Basics +import Messages.Comp.AddonArchiveForm +import Messages.Comp.AddonArchiveTable +import Messages.Comp.HttpError + + +type alias Texts = + { basics : Messages.Basics.Texts + , addonArchiveTable : Messages.Comp.AddonArchiveTable.Texts + , addonArchiveForm : Messages.Comp.AddonArchiveForm.Texts + , httpError : Http.Error -> String + , newAddonArchive : String + , reallyDeleteAddonArchive : String + , createNewAddonArchive : String + , deleteThisAddonArchive : String + , correctFormErrors : String + , installNow : String + , updateNow : String + , description : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , addonArchiveTable = Messages.Comp.AddonArchiveTable.gb + , addonArchiveForm = Messages.Comp.AddonArchiveForm.gb + , httpError = Messages.Comp.HttpError.gb + , newAddonArchive = "New Addon" + , reallyDeleteAddonArchive = "Really delete this Addon?" + , createNewAddonArchive = "Install new Addon" + , deleteThisAddonArchive = "Delete this Addon" + , correctFormErrors = "Please correct the errors in the form." + , installNow = "Install Addon" + , updateNow = "Update Addon" + , description = "Description" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , addonArchiveTable = Messages.Comp.AddonArchiveTable.de + , addonArchiveForm = Messages.Comp.AddonArchiveForm.de + , httpError = Messages.Comp.HttpError.de + , newAddonArchive = "Neues Addon" + , reallyDeleteAddonArchive = "Dieses Addon wirklich entfernen?" + , createNewAddonArchive = "Neues Addon installieren" + , deleteThisAddonArchive = "Addon löschen" + , correctFormErrors = "Bitte korrigiere die Fehler im Formular." + , installNow = "Addon Installieren" + , updateNow = "Addon aktualisieren" + , description = "Beschreibung" + } + + +fr : Texts +fr = + { basics = Messages.Basics.fr + , addonArchiveTable = Messages.Comp.AddonArchiveTable.fr + , addonArchiveForm = Messages.Comp.AddonArchiveForm.fr + , httpError = Messages.Comp.HttpError.fr + , newAddonArchive = "Nouveau favori" + , reallyDeleteAddonArchive = "Confirmer la suppression de ce favori ?" + , createNewAddonArchive = "Créer un nouveau favori" + , deleteThisAddonArchive = "Supprimer ce favori" + , correctFormErrors = "Veuillez corriger les erreurs du formulaire" + , installNow = "Installation de l'addon" + , updateNow = "Actualiser l'addon" + , description = "Description" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveTable.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveTable.elm new file mode 100644 index 00000000..47f95eee --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonArchiveTable.elm @@ -0,0 +1,42 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonArchiveTable exposing + ( Texts + , de + , fr + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , version : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , version = "Version" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , version = "Version" + } + + +fr : Texts +fr = + { basics = Messages.Basics.fr + , version = "Version" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigForm.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigForm.elm new file mode 100644 index 00000000..181239f9 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigForm.elm @@ -0,0 +1,108 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonRunConfigForm exposing + ( Texts + , de + , fr + , gb + ) + +import Data.TimeZone exposing (TimeZone) +import Messages.Basics +import Messages.Comp.CalEventInput + + +type alias Texts = + { basics : Messages.Basics.Texts + , calEventInput : Messages.Comp.CalEventInput.Texts + , enableDisable : String + , chooseName : String + , impersonateUser : String + , triggerRun : String + , schedule : String + , addons : String + , includedAddons : String + , add : String + , readMore : String + , readLess : String + , arguments : String + , update : String + , argumentsUpdated : String + , configureTitle : String + , configureLabel : String + } + + +gb : TimeZone -> Texts +gb tz = + { basics = Messages.Basics.gb + , calEventInput = Messages.Comp.CalEventInput.gb tz + , enableDisable = "Enable or disable this run configuration." + , chooseName = "Choose a name…" + , impersonateUser = "Run on behalf of user" + , triggerRun = "Trigger Run" + , schedule = "Schedule" + , addons = "Addons" + , includedAddons = "Included addons" + , add = "Add" + , readMore = "Read more" + , readLess = "Read less" + , arguments = "Arguments" + , update = "Update" + , argumentsUpdated = "Arguments updated" + , configureTitle = "Configure this addon" + , configureLabel = "Configure" + } + + +de : TimeZone -> Texts +de tz = + { basics = Messages.Basics.de + , calEventInput = Messages.Comp.CalEventInput.de tz + , enableDisable = "Konfiguration aktivieren oder deaktivieren" + , chooseName = "Name der Konfiguration…" + , impersonateUser = "Als Benutzer ausführen" + , triggerRun = "Auslöser" + , schedule = "Zeitplan" + , addons = "Addons" + , includedAddons = "Gewählte Addons" + , add = "Hinzufügen" + , readMore = "Mehr" + , readLess = "Weniger" + , arguments = "Argumente" + , update = "Aktualisieren" + , argumentsUpdated = "Argumente aktualisiert" + , configureTitle = "Konfiguriere dieses Addon" + , configureLabel = "Konfigurieren" + } + + + +-- TODO: translate-fr + + +fr : TimeZone -> Texts +fr tz = + { basics = Messages.Basics.fr + , calEventInput = Messages.Comp.CalEventInput.fr tz + , enableDisable = "Activer ou désactiver cette tâche." + , chooseName = "Choose a name…" + , impersonateUser = "Impersonate user" + , triggerRun = "Trigger Run" + , schedule = "Programmation" + , addons = "Addons" + , includedAddons = "Included addons" + , add = "Ajouter" + , readMore = "Read more" + , readLess = "Read less" + , arguments = "Arguments" + , update = "Update" + , argumentsUpdated = "Arguments updated" + , configureTitle = "Configure this addon" + , configureLabel = "Configure" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigManage.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigManage.elm new file mode 100644 index 00000000..b50a17fc --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigManage.elm @@ -0,0 +1,79 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonRunConfigManage exposing + ( Texts + , de + , fr + , gb + ) + +import Data.TimeZone exposing (TimeZone) +import Http +import Messages.Basics +import Messages.Comp.AddonRunConfigForm +import Messages.Comp.AddonRunConfigTable +import Messages.Comp.HttpError + + +type alias Texts = + { basics : Messages.Basics.Texts + , addonArchiveTable : Messages.Comp.AddonRunConfigTable.Texts + , addonArchiveForm : Messages.Comp.AddonRunConfigForm.Texts + , httpError : Http.Error -> String + , newAddonRunConfig : String + , reallyDeleteAddonRunConfig : String + , createNewAddonRunConfig : String + , deleteThisAddonRunConfig : String + , correctFormErrors : String + } + + +gb : TimeZone -> Texts +gb tz = + { basics = Messages.Basics.gb + , addonArchiveTable = Messages.Comp.AddonRunConfigTable.gb + , addonArchiveForm = Messages.Comp.AddonRunConfigForm.gb tz + , httpError = Messages.Comp.HttpError.gb + , newAddonRunConfig = "New" + , reallyDeleteAddonRunConfig = "Really delete this run config?" + , createNewAddonRunConfig = "Create a new run configuration" + , deleteThisAddonRunConfig = "Delete this run configuration" + , correctFormErrors = "Please correct the errors in the form." + } + + +de : TimeZone -> Texts +de tz = + { basics = Messages.Basics.de + , addonArchiveTable = Messages.Comp.AddonRunConfigTable.de + , addonArchiveForm = Messages.Comp.AddonRunConfigForm.de tz + , httpError = Messages.Comp.HttpError.de + , newAddonRunConfig = "Neu" + , reallyDeleteAddonRunConfig = "Dieses Konfiguration wirklich entfernen?" + , createNewAddonRunConfig = "Neue Run-Konfiguration erstellen" + , deleteThisAddonRunConfig = "Run-Konfiguration löschen" + , correctFormErrors = "Bitte korrigiere die Fehler im Formular." + } + + + +--- TODO translate-fr + + +fr : TimeZone -> Texts +fr tz = + { basics = Messages.Basics.fr + , addonArchiveTable = Messages.Comp.AddonRunConfigTable.fr + , addonArchiveForm = Messages.Comp.AddonRunConfigForm.fr tz + , httpError = Messages.Comp.HttpError.fr + , newAddonRunConfig = "Nouveau favori" + , reallyDeleteAddonRunConfig = "Confirmer la suppression de ce favori ?" + , createNewAddonRunConfig = "Créer un nouveau favori" + , deleteThisAddonRunConfig = "Supprimer ce favori" + , correctFormErrors = "Veuillez corriger les erreurs du formulaire" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigTable.elm b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigTable.elm new file mode 100644 index 00000000..02c4b417 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/AddonRunConfigTable.elm @@ -0,0 +1,50 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.AddonRunConfigTable exposing + ( Texts + , de + , fr + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , enabled : String + , trigger : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , enabled = "Enabled" + , trigger = "Triggered" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , enabled = "Aktive" + , trigger = "Auslöser" + } + + + +-- TODO translate-fr + + +fr : Texts +fr = + { basics = Messages.Basics.fr + , enabled = "Enabled" + , trigger = "Triggered" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm index 3ea89606..fca6e904 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm @@ -20,6 +20,7 @@ import Messages.Comp.ItemDetail.AddFilesForm import Messages.Comp.ItemDetail.ConfirmModal import Messages.Comp.ItemDetail.ItemInfoHeader import Messages.Comp.ItemDetail.Notes +import Messages.Comp.ItemDetail.RunAddonForm import Messages.Comp.ItemDetail.SingleAttachment import Messages.Comp.ItemLinkForm import Messages.Comp.ItemMail @@ -38,6 +39,7 @@ type alias Texts = , detailEdit : Messages.Comp.DetailEdit.Texts , confirmModal : Messages.Comp.ItemDetail.ConfirmModal.Texts , itemLinkForm : Messages.Comp.ItemLinkForm.Texts + , runAddonForm : Messages.Comp.ItemDetail.RunAddonForm.Texts , httpError : Http.Error -> String , key : String , backToSearchResults : String @@ -64,6 +66,8 @@ type alias Texts = , selectItem : String , deselectItem : String , relatedItems : String + , runAddonLabel : String + , runAddonTitle : String } @@ -78,6 +82,7 @@ gb tz = , detailEdit = Messages.Comp.DetailEdit.gb , confirmModal = Messages.Comp.ItemDetail.ConfirmModal.gb , itemLinkForm = Messages.Comp.ItemLinkForm.gb tz + , runAddonForm = Messages.Comp.ItemDetail.RunAddonForm.gb , httpError = Messages.Comp.HttpError.gb , key = "Key" , backToSearchResults = "Back to search results" @@ -104,6 +109,8 @@ gb tz = , selectItem = "Select this item" , deselectItem = "Deselect this item" , relatedItems = "Linked items" + , runAddonLabel = "Run addon" + , runAddonTitle = "Run an addon on this item" } @@ -118,6 +125,7 @@ de tz = , detailEdit = Messages.Comp.DetailEdit.de , confirmModal = Messages.Comp.ItemDetail.ConfirmModal.de , itemLinkForm = Messages.Comp.ItemLinkForm.de tz + , runAddonForm = Messages.Comp.ItemDetail.RunAddonForm.de , httpError = Messages.Comp.HttpError.de , key = "Taste" , backToSearchResults = "Zurück zur Suche" @@ -144,6 +152,8 @@ de tz = , selectItem = "Zur Auswahl hinzufügen" , deselectItem = "Aus Auswahl entfernen" , relatedItems = "Verknüpfte Dokumente" + , runAddonLabel = "Addon ausführen" + , runAddonTitle = "Addons für dieses Dokument ausführen" } @@ -158,6 +168,7 @@ fr tz = , detailEdit = Messages.Comp.DetailEdit.fr , confirmModal = Messages.Comp.ItemDetail.ConfirmModal.fr , itemLinkForm = Messages.Comp.ItemLinkForm.fr tz + , runAddonForm = Messages.Comp.ItemDetail.RunAddonForm.fr , httpError = Messages.Comp.HttpError.fr , key = "Clé" , backToSearchResults = "Retour aux résultat de recherche" @@ -184,4 +195,10 @@ fr tz = , selectItem = "Sélectionner ce document" , deselectItem = "Désélectionner ce document" , relatedItems = "Documents associés" + , runAddonLabel = "Run addon" + , runAddonTitle = "Run an addon on this item" } + + + +-- TODO translate-fr diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/RunAddonForm.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/RunAddonForm.elm new file mode 100644 index 00000000..9901c55b --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/RunAddonForm.elm @@ -0,0 +1,49 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ItemDetail.RunAddonForm exposing (Texts, de, fr, gb) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , runAddon : String + , addonRunConfig : String + , runAddonTitle : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , runAddon = "Run an addon" + , addonRunConfig = "Addon run configuration" + , runAddonTitle = "Run the selected addon on this item." + } + + +de : Texts +de = + { basics = Messages.Basics.de + , runAddon = "Addon ausführen" + , addonRunConfig = "Addon Konfiguration" + , runAddonTitle = "Run the selected addon on this item." + } + + + +-- TODO: translate-fr + + +fr : Texts +fr = + { basics = Messages.Basics.fr + , runAddon = "Run an addon" + , addonRunConfig = "Addon run configuration" + , runAddonTitle = "Run the selected addon on this item." + } diff --git a/modules/webapp/src/main/elm/Messages/Page/ManageData.elm b/modules/webapp/src/main/elm/Messages/Page/ManageData.elm index 5760f0f6..6ce47932 100644 --- a/modules/webapp/src/main/elm/Messages/Page/ManageData.elm +++ b/modules/webapp/src/main/elm/Messages/Page/ManageData.elm @@ -14,6 +14,8 @@ module Messages.Page.ManageData exposing import Data.TimeZone exposing (TimeZone) import Messages.Basics +import Messages.Comp.AddonArchiveManage +import Messages.Comp.AddonRunConfigManage import Messages.Comp.BookmarkManage import Messages.Comp.CustomFieldManage import Messages.Comp.EquipmentManage @@ -32,8 +34,12 @@ type alias Texts = , folderManage : Messages.Comp.FolderManage.Texts , customFieldManage : Messages.Comp.CustomFieldManage.Texts , bookmarkManage : Messages.Comp.BookmarkManage.Texts + , addonArchiveManage : Messages.Comp.AddonArchiveManage.Texts + , addonRunConfigManage : Messages.Comp.AddonRunConfigManage.Texts , manageData : String , bookmarks : String + , addonArchives : String + , addonRunConfigs : String } @@ -47,8 +53,12 @@ gb tz = , folderManage = Messages.Comp.FolderManage.gb tz , customFieldManage = Messages.Comp.CustomFieldManage.gb tz , bookmarkManage = Messages.Comp.BookmarkManage.gb + , addonArchiveManage = Messages.Comp.AddonArchiveManage.gb + , addonRunConfigManage = Messages.Comp.AddonRunConfigManage.gb tz , manageData = "Manage Data" , bookmarks = "Bookmarks" + , addonArchives = "Addons" + , addonRunConfigs = "Addon Run Configurations" } @@ -62,8 +72,12 @@ de tz = , folderManage = Messages.Comp.FolderManage.de tz , customFieldManage = Messages.Comp.CustomFieldManage.de tz , bookmarkManage = Messages.Comp.BookmarkManage.de + , addonArchiveManage = Messages.Comp.AddonArchiveManage.de + , addonRunConfigManage = Messages.Comp.AddonRunConfigManage.de tz , manageData = "Daten verwalten" , bookmarks = "Bookmarks" + , addonArchives = "Addons" + , addonRunConfigs = "Addon Run Configurations" } @@ -77,6 +91,10 @@ fr tz = , folderManage = Messages.Comp.FolderManage.fr tz , customFieldManage = Messages.Comp.CustomFieldManage.fr tz , bookmarkManage = Messages.Comp.BookmarkManage.fr + , addonArchiveManage = Messages.Comp.AddonArchiveManage.fr + , addonRunConfigManage = Messages.Comp.AddonRunConfigManage.fr tz , manageData = "Gestion des métadonnées" , bookmarks = "Favoris" + , addonArchives = "Addons" + , addonRunConfigs = "Addon Run Configurations" } diff --git a/modules/webapp/src/main/elm/Page/ManageData/Data.elm b/modules/webapp/src/main/elm/Page/ManageData/Data.elm index f56b8702..e0a0fd45 100644 --- a/modules/webapp/src/main/elm/Page/ManageData/Data.elm +++ b/modules/webapp/src/main/elm/Page/ManageData/Data.elm @@ -12,6 +12,8 @@ module Page.ManageData.Data exposing , init ) +import Comp.AddonArchiveManage +import Comp.AddonRunConfigManage import Comp.BookmarkManage import Comp.CustomFieldManage import Comp.EquipmentManage @@ -31,6 +33,8 @@ type alias Model = , folderManageModel : Comp.FolderManage.Model , fieldManageModel : Comp.CustomFieldManage.Model , bookmarkModel : Comp.BookmarkManage.Model + , addonArchiveModel : Comp.AddonArchiveManage.Model + , addonRunConfigModel : Comp.AddonRunConfigManage.Model } @@ -42,6 +46,12 @@ init flags = ( bm, bc ) = Comp.BookmarkManage.init flags + + ( aam, aac ) = + Comp.AddonArchiveManage.init flags + + ( arm, arc ) = + Comp.AddonRunConfigManage.init flags in ( { currentTab = Just TagTab , tagManageModel = m2 @@ -51,10 +61,14 @@ init flags = , folderManageModel = Comp.FolderManage.empty , fieldManageModel = Comp.CustomFieldManage.empty , bookmarkModel = bm + , addonArchiveModel = aam + , addonRunConfigModel = arm } , Cmd.batch [ Cmd.map TagManageMsg c2 , Cmd.map BookmarkMsg bc + , Cmd.map AddonArchiveMsg aac + , Cmd.map AddonRunConfigMsg arc ] ) @@ -67,6 +81,8 @@ type Tab | FolderTab | CustomFieldTab | BookmarkTab + | AddonArchiveTab + | AddonRunConfigTab type Msg @@ -78,3 +94,5 @@ type Msg | FolderMsg Comp.FolderManage.Msg | CustomFieldMsg Comp.CustomFieldManage.Msg | BookmarkMsg Comp.BookmarkManage.Msg + | AddonArchiveMsg Comp.AddonArchiveManage.Msg + | AddonRunConfigMsg Comp.AddonRunConfigManage.Msg diff --git a/modules/webapp/src/main/elm/Page/ManageData/Update.elm b/modules/webapp/src/main/elm/Page/ManageData/Update.elm index b2c61dd3..981f1b38 100644 --- a/modules/webapp/src/main/elm/Page/ManageData/Update.elm +++ b/modules/webapp/src/main/elm/Page/ManageData/Update.elm @@ -7,6 +7,8 @@ module Page.ManageData.Update exposing (update) +import Comp.AddonArchiveManage +import Comp.AddonRunConfigManage import Comp.BookmarkManage import Comp.CustomFieldManage import Comp.EquipmentManage @@ -15,11 +17,12 @@ import Comp.OrgManage import Comp.PersonManage import Comp.TagManage import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) import Page.ManageData.Data exposing (..) -update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -update flags msg model = +update : Flags -> UiSettings -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags uiSettings msg model = case msg of SetTab t -> let @@ -28,16 +31,16 @@ update flags msg model = in case t of TagTab -> - update flags (TagManageMsg Comp.TagManage.LoadTags) m + update flags uiSettings (TagManageMsg Comp.TagManage.LoadTags) m EquipTab -> - update flags (EquipManageMsg Comp.EquipmentManage.LoadEquipments) m + update flags uiSettings (EquipManageMsg Comp.EquipmentManage.LoadEquipments) m OrgTab -> - update flags (OrgManageMsg Comp.OrgManage.LoadOrgs) m + update flags uiSettings (OrgManageMsg Comp.OrgManage.LoadOrgs) m PersonTab -> - update flags (PersonManageMsg Comp.PersonManage.LoadPersons) m + update flags uiSettings (PersonManageMsg Comp.PersonManage.LoadPersons) m FolderTab -> let @@ -60,6 +63,20 @@ update flags msg model = in ( { m | bookmarkModel = bm }, Cmd.map BookmarkMsg bc, Sub.none ) + AddonArchiveTab -> + let + ( aam, aac ) = + Comp.AddonArchiveManage.init flags + in + ( { m | addonArchiveModel = aam }, Cmd.map AddonArchiveMsg aac, Sub.none ) + + AddonRunConfigTab -> + let + ( arm, arc ) = + Comp.AddonRunConfigManage.init flags + in + ( { m | addonRunConfigModel = arm }, Cmd.map AddonRunConfigMsg arc, Sub.none ) + TagManageMsg m -> let ( m2, c2 ) = @@ -117,3 +134,23 @@ update flags msg model = , Cmd.map BookmarkMsg c2 , Sub.map BookmarkMsg s2 ) + + AddonArchiveMsg lm -> + let + ( aam, aac, aas ) = + Comp.AddonArchiveManage.update flags lm model.addonArchiveModel + in + ( { model | addonArchiveModel = aam } + , Cmd.map AddonArchiveMsg aac + , Sub.map AddonArchiveMsg aas + ) + + AddonRunConfigMsg lm -> + let + ( arm, arc, ars ) = + Comp.AddonRunConfigManage.update flags uiSettings.timeZone lm model.addonRunConfigModel + in + ( { model | addonRunConfigModel = arm } + , Cmd.map AddonRunConfigMsg arc + , Sub.map AddonRunConfigMsg ars + ) diff --git a/modules/webapp/src/main/elm/Page/ManageData/View2.elm b/modules/webapp/src/main/elm/Page/ManageData/View2.elm index 2354a7c3..53f814dc 100644 --- a/modules/webapp/src/main/elm/Page/ManageData/View2.elm +++ b/modules/webapp/src/main/elm/Page/ManageData/View2.elm @@ -7,6 +7,8 @@ module Page.ManageData.View2 exposing (viewContent, viewSidebar) +import Comp.AddonArchiveManage +import Comp.AddonRunConfigManage import Comp.BookmarkManage import Comp.CustomFieldManage import Comp.EquipmentManage @@ -27,7 +29,7 @@ import Styles as S viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg -viewSidebar texts visible _ settings model = +viewSidebar texts visible flags settings model = div [ id "sidebar" , class S.sidebar @@ -134,6 +136,32 @@ viewSidebar texts visible _ settings model = [ text texts.bookmarks ] ] + , a + [ href "#" + , onClick (SetTab AddonArchiveTab) + , menuEntryActive model AddonArchiveTab + , class S.sidebarLink + , classList [ ( "hidden", not flags.config.addonsEnabled ) ] + ] + [ Icons.addonIcon "" + , span + [ class "ml-3" ] + [ text texts.addonArchives + ] + ] + , a + [ href "#" + , onClick (SetTab AddonRunConfigTab) + , menuEntryActive model AddonRunConfigTab + , class S.sidebarLink + , classList [ ( "hidden", not flags.config.addonsEnabled ) ] + ] + [ Icons.addonRunConfigIcon "" + , span + [ class "ml-3" ] + [ text texts.addonRunConfigs + ] + ] ] ] @@ -166,6 +194,20 @@ viewContent texts flags settings model = Just BookmarkTab -> viewBookmarks texts flags settings model + Just AddonArchiveTab -> + if flags.config.addonsEnabled then + viewAddonArchives texts flags settings model + + else + [] + + Just AddonRunConfigTab -> + if flags.config.addonsEnabled then + viewAddonRunConfigs texts flags settings model + + else + [] + Nothing -> [] ) @@ -306,3 +348,33 @@ viewBookmarks texts flags settings model = ] , Html.map BookmarkMsg (Comp.BookmarkManage.view texts.bookmarkManage settings flags model.bookmarkModel) ] + + +viewAddonArchives : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) +viewAddonArchives texts flags settings model = + [ h2 + [ class S.header1 + , class "inline-flex items-center" + ] + [ Icons.addonIcon "" + , div [ class "ml-2" ] + [ text texts.addonArchives + ] + ] + , Html.map AddonArchiveMsg (Comp.AddonArchiveManage.view texts.addonArchiveManage settings flags model.addonArchiveModel) + ] + + +viewAddonRunConfigs : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) +viewAddonRunConfigs texts flags settings model = + [ h2 + [ class S.header1 + , class "inline-flex items-center" + ] + [ Icons.addonRunConfigIcon "mr-4" + , div [ class "ml-2" ] + [ text texts.addonRunConfigs + ] + ] + , Html.map AddonRunConfigMsg (Comp.AddonRunConfigManage.view texts.addonRunConfigManage settings flags model.addonRunConfigModel) + ] diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index 48a866c4..a9a9304b 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -35,6 +35,11 @@ sidebarLink = " mb-2 px-4 py-3 flex flex-row hover:bg-blue-100 dark:hover:bg-slate-600 hover:font-bold rounded rounded-lg items-center " +successText : String +successText = + " text-green-600 dark:text-lime-500 " + + successMessage : String successMessage = " border border-green-600 bg-green-50 text-green-600 dark:border-lime-800 dark:bg-lime-300 dark:text-lime-800 px-4 py-2 rounded " @@ -281,7 +286,7 @@ link = inputErrorBorder : String inputErrorBorder = - " border-red-600 dark:border-orange-600 " + " ring dark:ring-0 ring-red-600 dark:border-orange-600 " inputLabel : String diff --git a/modules/webapp/src/main/elm/Util/List.elm b/modules/webapp/src/main/elm/Util/List.elm index 0c532bcb..ce7d1250 100644 --- a/modules/webapp/src/main/elm/Util/List.elm +++ b/modules/webapp/src/main/elm/Util/List.elm @@ -14,12 +14,42 @@ module Util.List exposing , findNext , findPrev , get + , removeByIndex + , replaceByIndex , sliding ) import Html.Attributes exposing (list) +removeByIndex : Int -> List a -> List a +removeByIndex index list = + List.indexedMap + (\idx -> + \e -> + if idx == index then + Nothing + + else + Just e + ) + list + |> List.filterMap identity + + +replaceByIndex : Int -> a -> List a -> List a +replaceByIndex index element list = + let + repl idx e = + if idx == index then + element + + else + e + in + List.indexedMap repl list + + changePosition : Int -> Int -> List a -> List a changePosition source target list = let diff --git a/modules/webapp/src/main/elm/Util/String.elm b/modules/webapp/src/main/elm/Util/String.elm index 54cfa9ea..a5972924 100644 --- a/modules/webapp/src/main/elm/Util/String.elm +++ b/modules/webapp/src/main/elm/Util/String.elm @@ -9,6 +9,7 @@ module Util.String exposing ( appendIfAbsent , crazyEncode , ellipsis + , firstSentenceOrMax , isBlank , isNothingOrBlank , underscoreToSpace @@ -16,7 +17,6 @@ module Util.String exposing ) import Base64 -import Html exposing (strong) crazyEncode : String -> String @@ -45,6 +45,26 @@ ellipsis len str = String.left (len - 1) str ++ "…" +firstSentenceOrMax : Int -> String -> Maybe String +firstSentenceOrMax maxLen str = + let + idx = + String.indexes "." str + |> List.head + |> Maybe.map ((+) 2) + |> Maybe.map (min maxLen) + |> Maybe.withDefault maxLen + + len = + String.length str + in + if len <= maxLen then + Nothing + + else + Just <| String.left (idx - 1) str ++ "…" + + withDefault : String -> String -> String withDefault default str = if str == "" then diff --git a/modules/webapp/src/main/styles/custom-components.css b/modules/webapp/src/main/styles/custom-components.css index 5daaf40b..afb3edca 100644 --- a/modules/webapp/src/main/styles/custom-components.css +++ b/modules/webapp/src/main/styles/custom-components.css @@ -96,4 +96,8 @@ .markdown-preview a { @apply text-blue-400 hover:text-blue-500 dark:text-sky-200 dark:hover:text-sky-100 cursor-pointer; } + + .markdown-preview pre { + @apply font-mono px-2 py-2 text-sm border dark:border-slate-600 rounded my-2; + } }