From dbd27057d197d6f16fd5d64f35696665d90454b0 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 3 Aug 2020 23:58:41 +0200 Subject: [PATCH] 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(