From ed8f16fe730b4ebac10dbf94b06ed23b296888e8 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 3 Aug 2020 18:27:13 +0200 Subject: [PATCH 1/2] Add a qr-code for source urls --- elm.json | 9 +++- .../webapp/src/main/elm/Comp/SourceForm.elm | 51 +++++++++++++++---- .../webapp/src/main/elm/Comp/SourceManage.elm | 8 +++ modules/webapp/src/main/webjar/docspell.css | 6 +++ website/shell.nix | 2 +- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/elm.json b/elm.json index 50606567..149303a7 100644 --- a/elm.json +++ b/elm.json @@ -21,15 +21,22 @@ "elm-explorations/markdown": "1.0.0", "justinmimbs/date": "3.1.2", "norpan/elm-html5-drag-drop": "3.1.4", + "pablohirafuji/elm-qrcode": "3.3.1", "ryannhg/date-format": "2.3.0", "truqu/elm-base64": "2.0.4", "ursi/elm-throttle": "1.0.1" }, "indirect": { + "avh4/elm-color": "1.0.0", + "danfishgold/base64-bytes": "1.0.3", "elm/bytes": "1.0.8", "elm/parser": "1.1.0", "elm/regex": "1.0.0", - "elm/virtual-dom": "1.0.2" + "elm/svg": "1.0.1", + "elm/virtual-dom": "1.0.2", + "elm-community/list-extra": "8.2.4", + "folkertdev/elm-flate": "2.0.4", + "justgook/elm-image": "4.0.0" } }, "test-dependencies": { diff --git a/modules/webapp/src/main/elm/Comp/SourceForm.elm b/modules/webapp/src/main/elm/Comp/SourceForm.elm index a5202ef4..4350ba32 100644 --- a/modules/webapp/src/main/elm/Comp/SourceForm.elm +++ b/modules/webapp/src/main/elm/Comp/SourceForm.elm @@ -23,6 +23,7 @@ import Html.Attributes exposing (..) import Html.Events exposing (onCheck, onInput) import Http import Markdown +import QRCode import Util.Folder exposing (mkFolderOption) @@ -97,6 +98,10 @@ type Msg | FolderDropdownMsg (Comp.Dropdown.Msg IdName) + +--- Update + + update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update flags msg model = case msg of @@ -223,6 +228,18 @@ update flags msg model = ( model_, Cmd.map FolderDropdownMsg c2 ) + +--- View + + +qrCodeView : String -> Html msg +qrCodeView message = + QRCode.encode message + |> Result.map QRCode.toSvg + |> Result.withDefault + (Html.text "Error generating QR-Code") + + view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = let @@ -299,16 +316,23 @@ disappear then. urlInfoMessage : Flags -> Model -> Html Msg urlInfoMessage flags model = + let + appUrl = + flags.config.baseUrl ++ "/app/upload/" ++ model.source.id + + apiUrl = + flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ model.source.id + in div [ classList [ ( "ui info icon message", True ) , ( "hidden", not model.enabled || model.source.id == "" ) ] ] - [ i [ class "info icon" ] [] - , div [ class "content" ] - [ div [ class "header" ] - [ text "Public Uploads" + [ div [ class "content" ] + [ h3 [ class "ui dividingheader" ] + [ i [ class "info icon" ] [] + , text "Public Uploads" ] , p [] [ text "This source defines URLs that can be used by anyone to send files to " @@ -318,15 +342,20 @@ urlInfoMessage flags model = , dl [ class "ui list" ] [ dt [] [ text "Public Upload Page" ] , dd [] - [ let - url = - flags.config.baseUrl ++ "/app/upload/" ++ model.source.id - in - a [ href url, target "_blank" ] [ code [] [ text url ] ] + [ a [ href appUrl, target "_blank" ] [ code [] [ text appUrl ] ] ] - , dt [] [ text "Public API Upload URL" ] + ] + , dl [ class "ui list" ] + [ dt [] [ text "Public API Upload URL" ] , dd [] - [ code [] [ text (flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ model.source.id) ] + [ p [] + [ code [] + [ text apiUrl + ] + ] + , p [ class "qr-code" ] + [ qrCodeView apiUrl + ] ] ] ] diff --git a/modules/webapp/src/main/elm/Comp/SourceManage.elm b/modules/webapp/src/main/elm/Comp/SourceManage.elm index 11184e42..69e25c3f 100644 --- a/modules/webapp/src/main/elm/Comp/SourceManage.elm +++ b/modules/webapp/src/main/elm/Comp/SourceManage.elm @@ -68,6 +68,10 @@ type Msg | RequestDelete + +--- Update + + update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update flags msg model = case msg of @@ -194,6 +198,10 @@ update flags msg model = ( { model | deleteConfirm = cm }, cmd ) + +--- View + + view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = if model.viewMode == Table then diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 20929c4a..8e726d4e 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -104,6 +104,12 @@ background: rgba(220, 255, 71, 0.6); } +.default-layout .qr-code svg { + border: 1px solid #ccc; + width: 200px; + height: 200px; +} + .markdown-preview { overflow: auto; max-height: 300px; diff --git a/website/shell.nix b/website/shell.nix index 4fed5990..e42b5750 100644 --- a/website/shell.nix +++ b/website/shell.nix @@ -1,5 +1,5 @@ let - nixpkgsUnstable = builtins.fetchTarball { + nixpkgsUnstable = builtins.fetchTarball { url = "https://github.com/NixOS/nixpkgs-channels/archive/nixos-unstable.tar.gz"; }; pkgsUnstable = import nixpkgsUnstable { }; From dbd27057d197d6f16fd5d64f35696665d90454b0 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 3 Aug 2020 23:58:41 +0200 Subject: [PATCH 2/2] Improve source view and add qrcode for urls The qr-code for urls is added so that these urls are easy to copy into a phone. Then buttons for copying them into the clipboard have been added. --- build.sbt | 3 +- .../restserver/webapp/TemplateRoutes.scala | 1 + .../webapp/src/main/elm/Comp/SourceForm.elm | 58 ----- .../webapp/src/main/elm/Comp/SourceManage.elm | 226 ++++++++++++++---- .../webapp/src/main/elm/Comp/SourceTable.elm | 89 ++++--- modules/webapp/src/main/elm/Ports.elm | 4 + modules/webapp/src/main/webjar/docspell.css | 4 +- modules/webapp/src/main/webjar/docspell.js | 10 + project/Dependencies.scala | 10 +- 9 files changed, 257 insertions(+), 148 deletions(-) diff --git a/build.sbt b/build.sbt index e5eb90f2..5538ba5a 100644 --- a/build.sbt +++ b/build.sbt @@ -616,9 +616,10 @@ def compileElm( def createWebjarSource(wj: Seq[ModuleID], out: File): Seq[File] = { val target = out / "Webjars.scala" + val badChars = "-.".toSet val fields = wj .map(m => - s"""val ${m.name.toLowerCase.filter(_ != '-')} = "/${m.name}/${m.revision}" """ + s"""val ${m.name.toLowerCase.filter(c => !badChars.contains(c))} = "/${m.name}/${m.revision}" """ ) .mkString("\n\n") val content = s"""package docspell.restserver.webapp diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala index ba46c2e5..b59c7875 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala @@ -126,6 +126,7 @@ object TemplateRoutes { Seq( "/app/assets" + Webjars.jquery + "/jquery.min.js", "/app/assets" + Webjars.semanticui + "/semantic.min.js", + "/app/assets" + Webjars.clipboardjs + "/clipboard.min.js", s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js" ), s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon", diff --git a/modules/webapp/src/main/elm/Comp/SourceForm.elm b/modules/webapp/src/main/elm/Comp/SourceForm.elm index 4350ba32..6d156ce8 100644 --- a/modules/webapp/src/main/elm/Comp/SourceForm.elm +++ b/modules/webapp/src/main/elm/Comp/SourceForm.elm @@ -23,7 +23,6 @@ import Html.Attributes exposing (..) import Html.Events exposing (onCheck, onInput) import Http import Markdown -import QRCode import Util.Folder exposing (mkFolderOption) @@ -232,14 +231,6 @@ update flags msg model = --- View -qrCodeView : String -> Html msg -qrCodeView message = - QRCode.encode message - |> Result.map QRCode.toSvg - |> Result.withDefault - (Html.text "Error generating QR-Code") - - view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = let @@ -310,55 +301,6 @@ disappear then. """ ] ] - , urlInfoMessage flags model - ] - - -urlInfoMessage : Flags -> Model -> Html Msg -urlInfoMessage flags model = - let - appUrl = - flags.config.baseUrl ++ "/app/upload/" ++ model.source.id - - apiUrl = - flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ model.source.id - in - div - [ classList - [ ( "ui info icon message", True ) - , ( "hidden", not model.enabled || model.source.id == "" ) - ] - ] - [ div [ class "content" ] - [ h3 [ class "ui dividingheader" ] - [ i [ class "info icon" ] [] - , text "Public Uploads" - ] - , p [] - [ text "This source defines URLs that can be used by anyone to send files to " - , text "you. There is a web page that you can share or the API url can be used " - , text "with other clients." - ] - , dl [ class "ui list" ] - [ dt [] [ text "Public Upload Page" ] - , dd [] - [ a [ href appUrl, target "_blank" ] [ code [] [ text appUrl ] ] - ] - ] - , dl [ class "ui list" ] - [ dt [] [ text "Public API Upload URL" ] - , dd [] - [ p [] - [ code [] - [ text apiUrl - ] - ] - , p [ class "qr-code" ] - [ qrCodeView apiUrl - ] - ] - ] - ] ] diff --git a/modules/webapp/src/main/elm/Comp/SourceManage.elm b/modules/webapp/src/main/elm/Comp/SourceManage.elm index 69e25c3f..70da2a06 100644 --- a/modules/webapp/src/main/elm/Comp/SourceManage.elm +++ b/modules/webapp/src/main/elm/Comp/SourceManage.elm @@ -8,10 +8,10 @@ module Comp.SourceManage exposing import Api import Api.Model.BasicResult exposing (BasicResult) -import Api.Model.Source +import Api.Model.Source exposing (Source) import Api.Model.SourceList exposing (SourceList) import Comp.SourceForm -import Comp.SourceTable +import Comp.SourceTable exposing (SelectMode(..)) import Comp.YesNoDimmer import Data.Flags exposing (Flags) import Data.UiSettings exposing (UiSettings) @@ -19,53 +19,64 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onSubmit) import Http +import Ports +import QRCode import Util.Http import Util.Maybe type alias Model = - { tableModel : Comp.SourceTable.Model - , formModel : Comp.SourceForm.Model - , viewMode : ViewMode + { formModel : Comp.SourceForm.Model + , viewMode : SelectMode , formError : Maybe String , loading : Bool , deleteConfirm : Comp.YesNoDimmer.Model + , sources : List Source } -type ViewMode - = Table - | Form - - init : Flags -> ( Model, Cmd Msg ) init flags = let ( fm, fc ) = Comp.SourceForm.init flags in - ( { tableModel = Comp.SourceTable.emptyModel - , formModel = fm - , viewMode = Table + ( { formModel = fm + , viewMode = None , formError = Nothing , loading = False , deleteConfirm = Comp.YesNoDimmer.emptyModel + , sources = [] } - , Cmd.map FormMsg fc + , Cmd.batch + [ Cmd.map FormMsg fc + , Ports.initClipboard appClipboardData + , Ports.initClipboard apiClipboardData + ] ) +appClipboardData : ( String, String ) +appClipboardData = + ( "app-url", "#app-url-copy-to-clipboard-btn" ) + + +apiClipboardData : ( String, String ) +apiClipboardData = + ( "api-url", "#api-url-copy-to-clipboard-btn" ) + + type Msg = TableMsg Comp.SourceTable.Msg | FormMsg Comp.SourceForm.Msg | LoadSources | SourceResp (Result Http.Error SourceList) - | SetViewMode ViewMode | InitNewSource | Submit | SubmitResp (Result Http.Error BasicResult) | YesNoMsg Comp.YesNoDimmer.Msg | RequestDelete + | SetTableView @@ -77,29 +88,31 @@ update flags msg model = case msg of TableMsg m -> let - ( tm, tc ) = - Comp.SourceTable.update flags m model.tableModel + ( tc, sel ) = + Comp.SourceTable.update flags m ( m2, c2 ) = ( { model - | tableModel = tm - , viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table + | viewMode = sel , formError = - if Util.Maybe.nonEmpty tm.selected then - Nothing + if Comp.SourceTable.isEdit sel then + model.formError else - model.formError + Nothing } , Cmd.map TableMsg tc ) ( m3, c3 ) = - case tm.selected of - Just source -> + case sel of + Edit source -> update flags (FormMsg (Comp.SourceForm.SetSource source)) m2 - Nothing -> + Display _ -> + ( m2, Cmd.none ) + + None -> ( m2, Cmd.none ) in ( m3, Cmd.batch [ c2, c3 ] ) @@ -115,34 +128,27 @@ update flags msg model = ( { model | loading = True }, Api.getSources flags SourceResp ) SourceResp (Ok sources) -> - let - m2 = - { model | viewMode = Table, loading = False } - in - update flags (TableMsg (Comp.SourceTable.SetSources sources.items)) m2 + ( { model + | viewMode = None + , loading = False + , sources = sources.items + } + , Cmd.none + ) SourceResp (Err _) -> ( { model | loading = False }, Cmd.none ) - SetViewMode m -> - let - m2 = - { model | viewMode = m } - in - case m of - Table -> - update flags (TableMsg Comp.SourceTable.Deselect) m2 - - Form -> - ( m2, Cmd.none ) + SetTableView -> + ( { model | viewMode = None }, Cmd.none ) InitNewSource -> let - nm = - { model | viewMode = Form, formError = Nothing } - source = Api.Model.Source.empty + + nm = + { model | viewMode = Edit source, formError = Nothing } in update flags (FormMsg (Comp.SourceForm.SetSource source)) nm @@ -164,7 +170,7 @@ update flags msg model = if res.success then let ( m2, c2 ) = - update flags (SetViewMode Table) model + update flags SetTableView model ( m3, c3 ) = update flags LoadSources m2 @@ -202,13 +208,25 @@ update flags msg model = --- View +qrCodeView : String -> Html msg +qrCodeView message = + QRCode.encode message + |> Result.map QRCode.toSvg + |> Result.withDefault + (Html.text "Error generating QR-Code") + + view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = - if model.viewMode == Table then - viewTable model + case model.viewMode of + None -> + viewTable model - else - div [] (viewForm flags settings model) + Edit _ -> + div [] (viewForm flags settings model) + + Display source -> + viewLinks flags settings source viewTable : Model -> Html Msg @@ -218,7 +236,7 @@ viewTable model = [ i [ class "plus icon" ] [] , text "Create new" ] - , Html.map TableMsg (Comp.SourceTable.view model.tableModel) + , Html.map TableMsg (Comp.SourceTable.view model.sources) , div [ classList [ ( "ui dimmer", True ) @@ -230,6 +248,112 @@ viewTable model = ] +viewLinks : Flags -> UiSettings -> Source -> Html Msg +viewLinks flags _ source = + let + appUrl = + flags.config.baseUrl ++ "/app/upload/" ++ source.id + + apiUrl = + flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ source.id + in + div + [] + [ h3 [ class "ui dividing header" ] + [ text "Public Uploads: " + , text source.abbrev + , div [ class "sub header" ] + [ text source.id + ] + ] + , p [] + [ text "This source defines URLs that can be used by anyone to send files to " + , text "you. There is a web page that you can share or the API url can be used " + , text "with other clients." + ] + , p [] + [ text "There have been " + , String.fromInt source.counter |> text + , text " items created through this source." + ] + , h4 [ class "ui header" ] + [ text "Public Upload Page" + ] + , div [ class "ui attached message" ] + [ div [ class "ui fluid left action input" ] + [ a + [ class "ui left icon button" + , title "Copy to clipboard" + , href "#" + , Tuple.second appClipboardData + |> String.dropLeft 1 + |> id + , attribute "data-clipboard-target" "#app-url" + ] + [ i [ class "copy icon" ] [] + ] + , a + [ class "ui icon button" + , href appUrl + , target "_blank" + , title "Open in new tab/window" + ] + [ i [ class "link external icon" ] [] + ] + , input + [ type_ "text" + , id "app-url" + , value appUrl + , readonly True + ] + [] + ] + ] + , div [ class "ui attached segment" ] + [ div [ class "qr-code" ] + [ qrCodeView appUrl + ] + ] + , h4 [ class "ui header" ] + [ text "Public API Upload URL" + ] + , div [ class "ui attached message" ] + [ div [ class "ui fluid left action input" ] + [ a + [ class "ui left icon button" + , title "Copy to clipboard" + , href "#" + , Tuple.second apiClipboardData + |> String.dropLeft 1 + |> id + , attribute "data-clipboard-target" "#api-url" + ] + [ i [ class "copy icon" ] [] + ] + , input + [ type_ "text" + , value apiUrl + , readonly True + , id "api-url" + ] + [] + ] + ] + , div [ class "ui attached segment" ] + [ div [ class "qr-code" ] + [ qrCodeView apiUrl + ] + ] + , div [ class "ui divider" ] [] + , button + [ class "ui button" + , onClick SetTableView + ] + [ text "Back" + ] + ] + + viewForm : Flags -> UiSettings -> Model -> List (Html Msg) viewForm flags settings model = let @@ -264,7 +388,7 @@ viewForm flags settings model = , button [ class "ui primary button", type_ "submit" ] [ text "Submit" ] - , a [ class "ui secondary button", onClick (SetViewMode Table), href "" ] + , a [ class "ui secondary button", onClick SetTableView, href "" ] [ text "Cancel" ] , if not newSource then diff --git a/modules/webapp/src/main/elm/Comp/SourceTable.elm b/modules/webapp/src/main/elm/Comp/SourceTable.elm index c6ab18a6..5e9d0ec8 100644 --- a/modules/webapp/src/main/elm/Comp/SourceTable.elm +++ b/modules/webapp/src/main/elm/Comp/SourceTable.elm @@ -1,7 +1,7 @@ module Comp.SourceTable exposing - ( Model - , Msg(..) - , emptyModel + ( Msg + , SelectMode(..) + , isEdit , update , view ) @@ -14,44 +14,47 @@ import Html.Attributes exposing (..) import Html.Events exposing (onClick) -type alias Model = - { sources : List Source - , selected : Maybe Source - } +type SelectMode + = Edit Source + | Display Source + | None -emptyModel : Model -emptyModel = - { sources = [] - , selected = Nothing - } +isEdit : SelectMode -> Bool +isEdit m = + case m of + Edit _ -> + True + + Display _ -> + False + + None -> + False type Msg - = SetSources (List Source) - | Select Source - | Deselect + = Select Source + | Show Source -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) -update _ msg model = +update : Flags -> Msg -> ( Cmd Msg, SelectMode ) +update _ msg = case msg of - SetSources list -> - ( { model | sources = list, selected = Nothing }, Cmd.none ) - Select source -> - ( { model | selected = Just source }, Cmd.none ) + ( Cmd.none, Edit source ) - Deselect -> - ( { model | selected = Nothing }, Cmd.none ) + Show source -> + ( Cmd.none, Display source ) -view : Model -> Html Msg -view model = - table [ class "ui selectable table" ] +view : List Source -> Html Msg +view sources = + table [ class "ui table" ] [ thead [] [ tr [] - [ th [ class "collapsing" ] [ text "Abbrev" ] + [ th [ class "collapsing" ] [] + , th [ class "collapsing" ] [ text "Abbrev" ] , th [ class "collapsing" ] [ text "Enabled" ] , th [ class "collapsing" ] [ text "Counter" ] , th [ class "collapsing" ] [ text "Priority" ] @@ -59,17 +62,37 @@ view model = ] ] , tbody [] - (List.map (renderSourceLine model) model.sources) + (List.map renderSourceLine sources) ] -renderSourceLine : Model -> Source -> Html Msg -renderSourceLine model source = +renderSourceLine : Source -> Html Msg +renderSourceLine source = tr - [ classList [ ( "active", model.selected == Just source ) ] - , onClick (Select source) - ] + [] [ td [ class "collapsing" ] + [ a + [ class "ui basic tiny primary button" + , href "#" + , onClick (Select source) + ] + [ i [ class "edit icon" ] [] + , text "Edit" + ] + , a + [ classList + [ ( "ui basic tiny primary button", True ) + , ( "disabled", not source.enabled ) + ] + , href "#" + , disabled (not source.enabled) + , onClick (Show source) + ] + [ i [ class "eye icon" ] [] + , text "Show" + ] + ] + , td [ class "collapsing" ] [ text source.abbrev ] , td [ class "collapsing" ] diff --git a/modules/webapp/src/main/elm/Ports.elm b/modules/webapp/src/main/elm/Ports.elm index 4e88cf5c..d401bc1a 100644 --- a/modules/webapp/src/main/elm/Ports.elm +++ b/modules/webapp/src/main/elm/Ports.elm @@ -1,5 +1,6 @@ port module Ports exposing ( getUiSettings + , initClipboard , loadUiSettings , onUiSettingsSaved , removeAccount @@ -78,3 +79,6 @@ getUiSettings flags = Nothing -> Cmd.none + + +port initClipboard : ( String, String ) -> Cmd msg diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 8e726d4e..630523c5 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -105,10 +105,12 @@ } .default-layout .qr-code svg { - border: 1px solid #ccc; width: 200px; height: 200px; } +.default-layout .qr-code.bordered svg { + border: 1px solid #ccc; +} .markdown-preview { overflow: auto; diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js index 6c1dacc5..cb91987f 100644 --- a/modules/webapp/src/main/webjar/docspell.js +++ b/modules/webapp/src/main/webjar/docspell.js @@ -83,3 +83,13 @@ elmApp.ports.requestUiSettings.subscribe(function(args) { } } }); + +var docspell_clipboards = {}; + +elmApp.ports.initClipboard.subscribe(function(args) { + var page = args[0]; + if (!docspell_clipboards[page]) { + var sel = args[1]; + docspell_clipboards[page] = new ClipboardJS(sel); + } +}); diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 63c1871e..35820efe 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,7 @@ object Dependencies { val BitpeaceVersion = "0.5.0" val CalevVersion = "0.3.1" val CirceVersion = "0.13.0" + val ClipboardJsVersion = "2.0.6" val DoobieVersion = "0.9.0" val EmilVersion = "0.6.2" val FastparseVersion = "2.1.3" @@ -245,10 +246,11 @@ object Dependencies { val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % BetterMonadicForVersion val webjars = Seq( - "org.webjars" % "swagger-ui" % SwaggerUIVersion, - "org.webjars" % "Semantic-UI" % SemanticUIVersion, - "org.webjars" % "jquery" % JQueryVersion, - "org.webjars" % "viewerjs" % ViewerJSVersion + "org.webjars" % "swagger-ui" % SwaggerUIVersion, + "org.webjars" % "Semantic-UI" % SemanticUIVersion, + "org.webjars" % "jquery" % JQueryVersion, + "org.webjars" % "viewerjs" % ViewerJSVersion, + "org.webjars" % "clipboard.js" % ClipboardJsVersion ) val icu4j = Seq(