diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm new file mode 100644 index 00000000..2a491f15 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -0,0 +1,292 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.PublishItems exposing + ( Model + , Msg + , Outcome(..) + , init + , initQuery + , update + , view + ) + +import Api +import Api.Model.IdResult exposing (IdResult) +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Basic as B +import Comp.MenuBar as MB +import Comp.ShareForm +import Comp.ShareView +import Data.Flags exposing (Flags) +import Data.Icons as Icons +import Data.ItemQuery exposing (ItemQuery) +import Data.SearchMode exposing (SearchMode) +import Html exposing (..) +import Html.Attributes exposing (..) +import Http +import Messages.Comp.PublishItems exposing (Texts) +import Ports +import Styles as S + + + +--- Model + + +type ViewMode + = ViewModeEdit + | ViewModeInfo ShareDetail + + +type FormError + = FormErrorNone + | FormErrorHttp Http.Error + | FormErrorInvalid + | FormErrorSubmit String + + +type alias Model = + { formModel : Comp.ShareForm.Model + , viewMode : ViewMode + , formError : FormError + , loading : Bool + } + + +init : ( Model, Cmd Msg ) +init = + let + ( fm, fc ) = + Comp.ShareForm.init + in + ( { formModel = fm + , viewMode = ViewModeEdit + , formError = FormErrorNone + , loading = False + } + , Cmd.map FormMsg fc + ) + + +initQuery : ItemQuery -> ( Model, Cmd Msg ) +initQuery query = + let + ( fm, fc ) = + Comp.ShareForm.initQuery (Data.ItemQuery.render query) + in + ( { formModel = fm + , viewMode = ViewModeEdit + , formError = FormErrorNone + , loading = False + } + , Cmd.map FormMsg fc + ) + + + +--- Update + + +type Msg + = FormMsg Comp.ShareForm.Msg + | CancelPublish + | SubmitPublish + | PublishResp (Result Http.Error IdResult) + | GetShareResp (Result Http.Error ShareDetail) + + +type Outcome + = OutcomeDone + | OutcomeInProgress + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , outcome : Outcome + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + CancelPublish -> + { model = model + , cmd = Cmd.none + , outcome = OutcomeDone + } + + FormMsg lm -> + let + ( fm, fc ) = + Comp.ShareForm.update flags lm model.formModel + in + { model = { model | formModel = fm } + , cmd = Cmd.map FormMsg fc + , outcome = OutcomeInProgress + } + + SubmitPublish -> + case Comp.ShareForm.getShare model.formModel of + Just ( _, data ) -> + { model = { model | loading = True } + , cmd = Api.addShare flags data PublishResp + , outcome = OutcomeInProgress + } + + Nothing -> + { model = { model | formError = FormErrorInvalid } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + PublishResp (Ok res) -> + if res.success then + { model = model + , cmd = Api.getShare flags res.id GetShareResp + , outcome = OutcomeInProgress + } + + else + { model = { model | formError = FormErrorSubmit res.message, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + PublishResp (Err err) -> + { model = { model | formError = FormErrorHttp err, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + GetShareResp (Ok share) -> + { model = + { model + | formError = FormErrorNone + , loading = False + , viewMode = ViewModeInfo share + } + , cmd = Ports.initClipboard (Comp.ShareView.clipboardData share) + , outcome = OutcomeInProgress + } + + GetShareResp (Err err) -> + { model = { model | formError = FormErrorHttp err, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + + +--- View + + +view : Texts -> Flags -> Model -> Html Msg +view texts flags model = + div [] + [ B.loadingDimmer + { active = model.loading + , label = "" + } + , case model.viewMode of + ViewModeEdit -> + viewForm texts model + + ViewModeInfo share -> + viewInfo texts flags model share + ] + + +viewInfo : Texts -> Flags -> Model -> ShareDetail -> Html Msg +viewInfo texts flags model share = + let + cfg = + { mainClasses = "" + , showAccessData = False + } + in + div [ class "px-2 mb-4" ] + [ h1 [ class S.header1 ] + [ text texts.title + ] + , div + [ class S.infoMessage + ] + [ text texts.infoText + ] + , MB.view <| + { start = + [ MB.SecondaryButton + { tagger = CancelPublish + , title = texts.cancelPublishTitle + , icon = Just "fa fa-arrow-left" + , label = texts.doneLabel + } + ] + , end = [] + , rootClasses = "my-4" + } + , div [] + [ Comp.ShareView.view cfg texts.shareView flags share + ] + ] + + +viewForm : Texts -> Model -> Html Msg +viewForm texts model = + div [ class "px-2 mb-4" ] + [ h1 [ class S.header1 ] + [ text texts.title + ] + , div + [ class S.infoMessage + ] + [ text texts.infoText + ] + , MB.view <| + { start = + [ MB.PrimaryButton + { tagger = SubmitPublish + , title = texts.submitPublishTitle + , icon = Just Icons.share + , label = texts.submitPublish + } + , MB.SecondaryButton + { tagger = CancelPublish + , title = texts.cancelPublishTitle + , icon = Just "fa fa-times" + , label = texts.cancelPublish + } + ] + , end = [] + , rootClasses = "my-4" + } + , div [] + [ Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + ] + , 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 + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ShareForm.elm b/modules/webapp/src/main/elm/Comp/ShareForm.elm index 4f2d39cf..d07e74a4 100644 --- a/modules/webapp/src/main/elm/Comp/ShareForm.elm +++ b/modules/webapp/src/main/elm/Comp/ShareForm.elm @@ -5,7 +5,7 @@ -} -module Comp.ShareForm exposing (Model, Msg, getShare, init, setShare, update, view) +module Comp.ShareForm exposing (Model, Msg, getShare, init, initQuery, setShare, update, view) import Api.Model.ShareData exposing (ShareData) import Api.Model.ShareDetail exposing (ShareDetail) @@ -36,16 +36,16 @@ type alias Model = } -init : ( Model, Cmd Msg ) -init = +initQuery : String -> ( Model, Cmd Msg ) +initQuery q = let ( dp, dpc ) = Comp.DatePicker.init in ( { share = Api.Model.ShareDetail.empty , name = Nothing - , query = "" - , enabled = False + , query = q + , enabled = True , passwordModel = Comp.PasswordInput.init , password = Nothing , passwordSet = False @@ -57,6 +57,11 @@ init = ) +init : ( Model, Cmd Msg ) +init = + initQuery "" + + isValid : Model -> Bool isValid model = model.query /= "" && model.untilDate /= Nothing @@ -206,7 +211,7 @@ view texts model = , class S.textInput , classList [ ( S.inputErrorBorder - , not (isValid model) + , model.query == "" ) ] ] @@ -265,12 +270,16 @@ view texts model = ] ] ] - , div [ class "mb-2 max-w-sm" ] + , div + [ class "mb-2 max-w-sm" + ] [ label [ class S.inputLabel ] [ text texts.publishUntil , B.inputRequired ] - , div [ class "relative" ] + , div + [ class "relative" + ] [ Html.map UntilDateMsg (Comp.DatePicker.viewTimeDefault model.untilDate @@ -278,5 +287,15 @@ view texts model = ) , i [ class S.dateInputIcon, class "fa fa-calendar" ] [] ] + , div + [ classList + [ ( "hidden" + , model.untilDate /= Nothing + ) + ] + , class "mt-1" + , class S.errorText + ] + [ text "This field is required." ] ] ] diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index 4fc431af..472680c4 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -17,12 +17,14 @@ import Comp.ItemDetail.Model exposing (Msg(..)) import Comp.MenuBar as MB import Comp.ShareForm import Comp.ShareTable +import Comp.ShareView import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) import Http import Messages.Comp.ShareManage exposing (Texts) +import Ports import Styles as S @@ -107,7 +109,7 @@ update flags msg model = share = Api.Model.ShareDetail.empty in - update flags (FormMsg (Comp.ShareForm.setShare share)) nm + update flags (FormMsg (Comp.ShareForm.setShare { share | enabled = True })) nm SetViewMode vm -> ( { model | viewMode = vm, formError = FormErrorNone } @@ -129,13 +131,10 @@ update flags msg model = let action = Comp.ShareTable.update lm - - nextModel = - { model | viewMode = Form, formError = FormErrorNone } in case action of Comp.ShareTable.Edit share -> - update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + setShare share flags model RequestDelete -> ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none ) @@ -190,11 +189,7 @@ update flags msg model = ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) GetShareResp (Ok share) -> - let - nextModel = - { model | formError = FormErrorNone, loading = False } - in - update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + setShare share flags model GetShareResp (Err err) -> ( { model | formError = FormErrorHttp err }, Cmd.none ) @@ -210,17 +205,32 @@ update flags msg model = ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none ) +setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg ) +setShare share flags model = + let + nextModel = + { model | formError = FormErrorNone, viewMode = Form, loading = False } + + initClipboard = + Ports.initClipboard (Comp.ShareView.clipboardData share) + + ( nm, nc ) = + update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + in + ( nm, Cmd.batch [ initClipboard, nc ] ) + + --- view view : Texts -> Flags -> Model -> Html Msg -view texts _ model = +view texts flags model = if model.viewMode == Table then viewTable texts model else - viewForm texts model + viewForm texts flags model viewTable : Texts -> Model -> Html Msg @@ -247,103 +257,119 @@ viewTable texts model = ] -viewForm : Texts -> Model -> Html Msg -viewForm texts model = +viewForm : Texts -> Flags -> Model -> Html Msg +viewForm texts flags model = let newShare = model.formModel.share.id == "" in - Html.form [ class "relative" ] - [ if newShare then - h1 [ class S.header2 ] - [ text texts.createNewShare - ] - - else - h1 [ class S.header2 ] - [ text <| Maybe.withDefault texts.noName model.formModel.share.name - , div [ class "opacity-50 text-sm" ] - [ text "Id: " - , text model.formModel.share.id + div [ class "relative" ] + [ Html.form [] + [ if newShare then + h1 [ class S.header2 ] + [ text texts.createNewShare ] - ] - , MB.view - { start = - [ MB.PrimaryButton - { tagger = Submit - , title = "Submit this form" - , icon = Just "fa fa-save" - , label = texts.basics.submit - } - , MB.SecondaryButton - { tagger = SetViewMode Table - , title = texts.basics.backToList - , icon = Just "fa fa-arrow-left" - , label = texts.basics.cancel - } - ] - , end = - if not newShare then - [ MB.DeleteButton - { tagger = RequestDelete - , title = texts.deleteThisShare - , icon = Just "fa fa-trash" - , label = texts.basics.delete + + else + h1 [ class S.header2 ] + [ text <| Maybe.withDefault texts.noName model.formModel.share.name + , div [ class "opacity-50 text-sm" ] + [ text "Id: " + , text model.formModel.share.id + ] + ] + , MB.view + { start = + [ MB.PrimaryButton + { tagger = Submit + , title = "Submit this form" + , icon = Just "fa fa-save" + , label = texts.basics.submit + } + , MB.SecondaryButton + { tagger = SetViewMode Table + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.cancel } ] + , end = + if not newShare then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisShare + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] - else - [] - , rootClasses = "mb-4" - } - , div - [ classList - [ ( "hidden", model.formError == FormErrorNone ) + else + [] + , rootClasses = "mb-4" + } + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage ] - , 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 + ] + , Html.map FormMsg (Comp.ShareForm.view texts.shareForm 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.reallyDeleteShare + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteShareNow model.formModel.share.id) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) ] - [ case model.formError of - FormErrorNone -> - text "" - - FormErrorHttp err -> - text (texts.httpError err) - - FormErrorInvalid -> - text texts.correctFormErrors - - FormErrorSubmit m -> - text m - ] - , Html.map FormMsg (Comp.ShareForm.view texts.shareForm 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.reallyDeleteShare - ] - , div [ class "mt-4 flex flex-row items-center" ] - [ B.deleteButton - { label = texts.basics.yes - , icon = "fa fa-check" - , disabled = False - , handler = onClick (DeleteShareNow model.formModel.share.id) - , attrs = [ href "#" ] - } - , B.secondaryButton - { label = texts.basics.no - , icon = "fa fa-times" - , disabled = False - , handler = onClick CancelDelete - , attrs = [ href "#", class "ml-2" ] - } - ] - ] - ) + , shareInfo texts flags model.formModel.share + ] + + +shareInfo : Texts -> Flags -> ShareDetail -> Html Msg +shareInfo texts flags share = + div + [ class "mt-6" + , classList [ ( "hidden", share.id == "" ) ] + ] + [ h2 [ class S.header2 ] + [ text texts.shareInformation + ] + , Comp.ShareView.viewDefault texts.shareView flags share ] diff --git a/modules/webapp/src/main/elm/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Comp/ShareTable.elm index 940f64c5..b62f39b5 100644 --- a/modules/webapp/src/main/elm/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Comp/ShareTable.elm @@ -54,7 +54,7 @@ view texts shares = [ text texts.basics.name ] , th [ class "text-center" ] - [ text texts.enabled + [ text texts.active ] , th [ class "text-center" ] [ text texts.publishUntil @@ -79,7 +79,14 @@ renderShareLine texts share = [ text (Maybe.withDefault "-" share.name) ] , td [ class "w-px px-2 text-center" ] - [ Util.Html.checkbox2 share.enabled + [ if not share.enabled then + i [ class "fa fa-ban" ] [] + + else if share.expired then + i [ class "fa fa-bolt text-red-600 dark:text-orange-800" ] [] + + else + i [ class "fa fa-check" ] [] ] , td [ class "hidden sm:table-cell text-center" ] [ texts.formatDateTime share.publishUntil |> text diff --git a/modules/webapp/src/main/elm/Comp/ShareView.elm b/modules/webapp/src/main/elm/Comp/ShareView.elm new file mode 100644 index 00000000..f7d4962f --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareView.elm @@ -0,0 +1,184 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareView exposing (ViewSettings, clipboardData, view, viewDefault) + +import Api.Model.ShareDetail exposing (ShareDetail) +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Comp.ShareView exposing (Texts) +import QRCode +import Styles as S + + +type alias ViewSettings = + { mainClasses : String + , showAccessData : Bool + } + + +view : ViewSettings -> Texts -> Flags -> ShareDetail -> Html msg +view cfg texts flags share = + if not share.enabled then + viewDisabled cfg texts share + + else if share.expired then + viewExpired cfg texts share + + else + viewActive cfg texts flags share + + +viewDefault : Texts -> Flags -> ShareDetail -> Html msg +viewDefault = + view + { mainClasses = "" + , showAccessData = True + } + + +clipboardData : ShareDetail -> ( String, String ) +clipboardData share = + ( "app-share-" ++ share.id, "#app-share-url-copy-to-clipboard-btn-" ++ share.id ) + + + +--- Helper + + +viewActive : ViewSettings -> Texts -> Flags -> ShareDetail -> Html msg +viewActive cfg texts flags share = + let + clipboard = + clipboardData share + + appUrl = + flags.config.baseUrl ++ "/app/share/" ++ share.id + + styleUrl = + "truncate px-2 py-2 border-0 border-t border-b border-r font-mono text-sm my-auto rounded-r border-gray-400 dark:border-bluegray-500" + + infoLine hidden icon label value = + div + [ class "flex flex-row items-center" + , classList [ ( "hidden", hidden ) ] + ] + [ div [ class "flex mr-3" ] + [ i [ class icon ] [] + ] + , div [ class "flex flex-col" ] + [ div [ class "-mb-1" ] + [ text value + ] + , div [ class "opacity-50 text-sm" ] + [ text label + ] + ] + ] + in + div + [ class cfg.mainClasses + , class "flex flex-col sm:flex-row " + ] + [ div [ class "flex" ] + [ div + [ class S.border + , class S.qrCode + ] + [ qrCodeView texts appUrl + ] + ] + , div + [ class "flex flex-col ml-3 pr-2" + + -- hack for the qr code that is 265px + , style "max-width" "calc(100% - 265px)" + ] + [ div [ class "font-medium text-2xl" ] + [ text <| Maybe.withDefault texts.noName share.name + ] + , div [ class "my-2" ] + [ div [ class "flex flex-row" ] + [ a + [ class S.secondaryBasicButtonPlain + , class "rounded-l border text-sm px-4 py-2" + , title texts.copyToClipboard + , href "#" + , Tuple.second clipboard + |> String.dropLeft 1 + |> id + , attribute "data-clipboard-target" ("#" ++ Tuple.first clipboard) + ] + [ i [ class "fa fa-copy" ] [] + ] + , a + [ class S.secondaryBasicButtonPlain + , class "px-4 py-2 border-0 border-t border-b border-r text-sm" + , href appUrl + , target "_blank" + , title texts.openInNewTab + ] + [ i [ class "fa fa-external-link-alt" ] [] + ] + , div + [ id (Tuple.first clipboard) + , class styleUrl + ] + [ text appUrl + ] + ] + ] + , div [ class "text-lg flex flex-col" ] + [ infoLine False "fa fa-calendar" texts.publishUntil (texts.date share.publishUntil) + , infoLine False + (if share.password then + "fa fa-lock" + + else + "fa fa-lock-open" + ) + texts.passwordProtected + (if share.password then + texts.basics.yes + + else + texts.basics.no + ) + , infoLine + (not cfg.showAccessData) + "fa fa-eye" + texts.views + (String.fromInt share.views) + , infoLine + (not cfg.showAccessData) + "fa fa-calendar-check font-thin" + texts.lastAccess + (Maybe.map texts.date share.lastAccess |> Maybe.withDefault "-") + ] + ] + ] + + +viewExpired : ViewSettings -> Texts -> ShareDetail -> Html msg +viewExpired cfg texts share = + div [ class S.warnMessage ] + [ text texts.expiredInfo ] + + +viewDisabled : ViewSettings -> Texts -> ShareDetail -> Html msg +viewDisabled cfg texts share = + div [ class S.warnMessage ] + [ text texts.disabledInfo ] + + +qrCodeView : Texts -> String -> Html msg +qrCodeView texts message = + QRCode.encode message + |> Result.map QRCode.toSvg + |> Result.withDefault + (Html.text texts.qrCodeError) diff --git a/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm new file mode 100644 index 00000000..269f68ef --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm @@ -0,0 +1,82 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.PublishItems exposing + ( Texts + , de + , gb + ) + +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Comp.ShareForm +import Messages.Comp.ShareView +import Messages.DateFormat +import Messages.UiLanguage + + +type alias Texts = + { basics : Messages.Basics.Texts + , httpError : Http.Error -> String + , shareForm : Messages.Comp.ShareForm.Texts + , shareView : Messages.Comp.ShareView.Texts + , title : String + , infoText : String + , formatDateLong : Int -> String + , formatDateShort : Int -> String + , submitPublish : String + , cancelPublish : String + , submitPublishTitle : String + , cancelPublishTitle : String + , publishSuccessful : String + , publishInProcess : String + , correctFormErrors : String + , doneLabel : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , httpError = Messages.Comp.HttpError.gb + , shareForm = Messages.Comp.ShareForm.gb + , shareView = Messages.Comp.ShareView.gb + , title = "Publish Items" + , infoText = "Publishing items creates a cryptic link, which can be used by everyone to see the selected documents. This link cannot be guessed, but is public! It exists for a certain amount of time and can be further protected using a password." + , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.English + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.English + , submitPublish = "Publish" + , submitPublishTitle = "Publish the documents now" + , cancelPublish = "Cancel" + , cancelPublishTitle = "Back to select view" + , publishSuccessful = "Items published successfully" + , publishInProcess = "Items are published …" + , correctFormErrors = "Please correct the errors in the form." + , doneLabel = "Done" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , httpError = Messages.Comp.HttpError.de + , shareForm = Messages.Comp.ShareForm.de + , shareView = Messages.Comp.ShareView.de + , title = "Dokumente publizieren" + , infoText = "Beim Publizieren der Dokumente wird ein kryptischer Link erzeugt, mit welchem jeder die dahinter publizierten Dokumente einsehen kann. Dieser Link kann nicht erraten werden, ist aber öffentlich. Er ist zeitlich begrenzt und kann zusätzlich mit einem Passwort geschützt werden." + , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.German + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.German + , submitPublish = "Publizieren" + , submitPublishTitle = "Dokumente jetzt publizieren" + , cancelPublish = "Abbrechen" + , cancelPublishTitle = "Zurück zur Auswahl" + , publishSuccessful = "Die Dokumente wurden erfolgreich publiziert." + , publishInProcess = "Dokumente werden publiziert…" + , correctFormErrors = "Bitte korrigiere die Fehler im Formular." + , doneLabel = "Fertig" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm index 66b27e6e..c415d3c8 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -16,12 +16,14 @@ import Messages.Basics import Messages.Comp.HttpError import Messages.Comp.ShareForm import Messages.Comp.ShareTable +import Messages.Comp.ShareView type alias Texts = { basics : Messages.Basics.Texts , shareTable : Messages.Comp.ShareTable.Texts , shareForm : Messages.Comp.ShareForm.Texts + , shareView : Messages.Comp.ShareView.Texts , httpError : Http.Error -> String , newShare : String , copyToClipboard : String @@ -33,6 +35,7 @@ type alias Texts = , errorGeneratingQR : String , correctFormErrors : String , noName : String + , shareInformation : String } @@ -42,6 +45,7 @@ gb = , httpError = Messages.Comp.HttpError.gb , shareTable = Messages.Comp.ShareTable.gb , shareForm = Messages.Comp.ShareForm.gb + , shareView = Messages.Comp.ShareView.gb , newShare = "New share" , copyToClipboard = "Copy to clipboard" , openInNewTab = "Open in new tab/window" @@ -52,6 +56,7 @@ gb = , errorGeneratingQR = "Error generating QR Code" , correctFormErrors = "Please correct the errors in the form." , noName = "No Name" + , shareInformation = "Share Information" } @@ -60,6 +65,7 @@ de = { basics = Messages.Basics.de , shareTable = Messages.Comp.ShareTable.de , shareForm = Messages.Comp.ShareForm.de + , shareView = Messages.Comp.ShareView.de , httpError = Messages.Comp.HttpError.de , newShare = "Neue Freigabe" , copyToClipboard = "In die Zwischenablage kopieren" @@ -71,4 +77,5 @@ de = , errorGeneratingQR = "Fehler beim Generieren des QR-Code" , correctFormErrors = "Bitte korrigiere die Fehler im Formular." , noName = "Ohne Name" + , shareInformation = "Informationen zur Freigabe" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm index 7b68fcc4..5b87e47e 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm @@ -19,7 +19,7 @@ import Messages.UiLanguage type alias Texts = { basics : Messages.Basics.Texts , formatDateTime : Int -> String - , enabled : String + , active : String , publishUntil : String } @@ -28,7 +28,7 @@ gb : Texts gb = { basics = Messages.Basics.gb , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English - , enabled = "Enabled" + , active = "Active" , publishUntil = "Publish Until" } @@ -37,6 +37,6 @@ de : Texts de = { basics = Messages.Basics.de , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German - , enabled = "Aktiv" + , active = "Aktiv" , publishUntil = "Publiziert bis" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm new file mode 100644 index 00000000..86f15c07 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm @@ -0,0 +1,66 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareView exposing + ( Texts + , de + , gb + ) + +import Messages.Basics +import Messages.DateFormat as DF +import Messages.UiLanguage + + +type alias Texts = + { basics : Messages.Basics.Texts + , date : Int -> String + , qrCodeError : String + , expiredInfo : String + , disabledInfo : String + , noName : String + , copyToClipboard : String + , openInNewTab : String + , publishUntil : String + , passwordProtected : String + , views : String + , lastAccess : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , date = DF.formatDateLong Messages.UiLanguage.English + , qrCodeError = "Error generating QR Code." + , expiredInfo = "This share has expired." + , disabledInfo = "This share is disabled." + , noName = "No Name" + , copyToClipboard = "Copy to clipboard" + , openInNewTab = "Open in new tab/window" + , publishUntil = "Published Until" + , passwordProtected = "Password protected" + , views = "Views" + , lastAccess = "Last Access" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , date = DF.formatDateLong Messages.UiLanguage.German + , qrCodeError = "Fehler beim Erzeugen des QR-Codes." + , expiredInfo = "Diese Freigabe ist abgelaufen." + , disabledInfo = "Diese Freigae ist nicht aktiv." + , noName = "Ohne Name" + , copyToClipboard = "In die Zwischenablage kopieren" + , openInNewTab = "Im neuen Tab/Fenster öffnen" + , publishUntil = "Publiziert bis" + , passwordProtected = "Passwordgeschützt" + , views = "Aufrufe" + , lastAccess = "Letzter Zugriff" + } diff --git a/modules/webapp/src/main/elm/Messages/Page/Home.elm b/modules/webapp/src/main/elm/Messages/Page/Home.elm index f51c5202..dada7a27 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Home.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Home.elm @@ -14,6 +14,7 @@ module Messages.Page.Home exposing import Messages.Basics import Messages.Comp.ItemCardList import Messages.Comp.ItemMerge +import Messages.Comp.PublishItems import Messages.Comp.SearchStatsView import Messages.Page.HomeSideMenu @@ -24,6 +25,7 @@ type alias Texts = , searchStatsView : Messages.Comp.SearchStatsView.Texts , sideMenu : Messages.Page.HomeSideMenu.Texts , itemMerge : Messages.Comp.ItemMerge.Texts + , publishItems : Messages.Comp.PublishItems.Texts , contentSearch : String , searchInNames : String , selectModeTitle : String @@ -42,6 +44,11 @@ type alias Texts = , resetSearchForm : String , exitSelectMode : String , mergeItemsTitle : Int -> String + , publishItemsTitle : Int -> String + , publishCurrentQueryTitle : String + , nothingSelectedToShare : String + , loadMore : String + , thatsAll : String } @@ -52,6 +59,7 @@ gb = , searchStatsView = Messages.Comp.SearchStatsView.gb , sideMenu = Messages.Page.HomeSideMenu.gb , itemMerge = Messages.Comp.ItemMerge.gb + , publishItems = Messages.Comp.PublishItems.gb , contentSearch = "Content search…" , searchInNames = "Search in names…" , selectModeTitle = "Select Mode" @@ -70,6 +78,11 @@ gb = , resetSearchForm = "Reset search form" , exitSelectMode = "Exit Select Mode" , mergeItemsTitle = \n -> "Merge " ++ String.fromInt n ++ " selected items" + , publishItemsTitle = \n -> "Publish " ++ String.fromInt n ++ " selected items" + , publishCurrentQueryTitle = "Publish current results" + , nothingSelectedToShare = "Sharing everything doesn't work. You need to apply some criteria." + , loadMore = "Load more…" + , thatsAll = "That's all" } @@ -80,6 +93,7 @@ de = , searchStatsView = Messages.Comp.SearchStatsView.de , sideMenu = Messages.Page.HomeSideMenu.de , itemMerge = Messages.Comp.ItemMerge.de + , publishItems = Messages.Comp.PublishItems.de , contentSearch = "Volltextsuche…" , searchInNames = "Suche in Namen…" , selectModeTitle = "Auswahlmodus" @@ -98,4 +112,9 @@ de = , resetSearchForm = "Suchformular zurücksetzen" , exitSelectMode = "Auswahlmodus verlassen" , mergeItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente zusammenführen" + , publishItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente publizieren" + , publishCurrentQueryTitle = "Aktuelle Ansicht publizieren" + , nothingSelectedToShare = "Alles kann nicht geteilt werden; es muss etwas gesucht werden." + , loadMore = "Mehr laden…" + , thatsAll = "Mehr gibt es nicht" } diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index cb60492d..c4b65df0 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -14,6 +14,7 @@ module Page.Home.Data exposing , SelectActionMode(..) , SelectViewModel , ViewMode(..) + , createQuery , doSearchCmd , editActive , init @@ -36,6 +37,7 @@ import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) import Comp.ItemMerge import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Data.Flags exposing (Flags) import Data.ItemNav exposing (ItemNav) @@ -79,6 +81,7 @@ type alias SelectViewModel = , confirmModal : Maybe ConfirmModalValue , editModel : Comp.ItemDetail.MultiEditMenu.Model , mergeModel : Comp.ItemMerge.Model + , publishModel : Comp.PublishItems.Model , saveNameState : SaveNameState , saveCustomFieldState : Set String } @@ -91,6 +94,7 @@ initSelectViewModel = , confirmModal = Nothing , editModel = Comp.ItemDetail.MultiEditMenu.init , mergeModel = Comp.ItemMerge.init [] + , publishModel = Tuple.first Comp.PublishItems.init , saveNameState = SaveSuccess , saveCustomFieldState = Set.empty } @@ -100,6 +104,7 @@ type ViewMode = SimpleView | SearchView | SelectView SelectViewModel + | PublishView Comp.PublishItems.Model init : Flags -> ViewMode -> Model @@ -143,6 +148,9 @@ menuCollapsed model = SelectView _ -> False + PublishView _ -> + False + selectActive : Model -> Bool selectActive model = @@ -153,6 +161,9 @@ selectActive model = SearchView -> False + PublishView _ -> + False + SelectView _ -> True @@ -166,6 +177,9 @@ editActive model = SearchView -> False + PublishView _ -> + False + SelectView svm -> svm.action == EditSelected @@ -211,6 +225,10 @@ type Msg | RemoveItem String | MergeSelectedItems | MergeItemsMsg Comp.ItemMerge.Msg + | PublishSelectedItems + | PublishItemsMsg Comp.PublishItems.Msg + | TogglePublishCurrentQueryView + | PublishViewMsg Comp.PublishItems.Msg type SearchType @@ -225,6 +243,7 @@ type SelectActionMode | ReprocessSelected | RestoreSelected | MergeSelected + | PublishSelected type alias SearchParam = @@ -251,10 +270,7 @@ doSearchDefaultCmd param model = let smask = Q.request model.searchMenuModel.searchMode <| - Q.and - [ Comp.SearchMenu.getItemQuery model.searchMenuModel - , Maybe.map Q.Fragment model.powerSearchInput.input - ] + createQuery model mask = { smask @@ -272,6 +288,14 @@ doSearchDefaultCmd param model = Api.itemSearch param.flags mask ItemSearchAddResp +createQuery : Model -> Maybe Q.ItemQuery +createQuery model = + Q.and + [ Comp.SearchMenu.getItemQuery model.searchMenuModel + , Maybe.map Q.Fragment model.powerSearchInput.input + ] + + resultsBelowLimit : UiSettings -> Model -> Bool resultsBelowLimit settings model = let diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 2ba19438..00ebfe9a 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -19,6 +19,7 @@ import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) import Comp.ItemMerge import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Data.Flags exposing (Flags) import Data.ItemQuery as Q @@ -237,6 +238,9 @@ update mId key flags settings msg model = SelectView _ -> SimpleView + + PublishView q -> + PublishView q in withSub ( { model | viewMode = nextView } @@ -255,6 +259,9 @@ update mId key flags settings msg model = SelectView _ -> ( SearchView, Cmd.none ) + + PublishView q -> + ( PublishView q, Cmd.none ) in withSub ( { model @@ -620,6 +627,85 @@ update mId key flags settings msg model = _ -> noSub ( model, Cmd.none ) + PublishSelectedItems -> + case model.viewMode of + SelectView svm -> + if svm.action == PublishSelected then + let + ( mm, mc ) = + Comp.PublishItems.init + in + noSub + ( { model + | viewMode = + SelectView + { svm + | action = NoneAction + , publishModel = mm + } + } + , Cmd.map PublishItemsMsg mc + ) + + else if svm.ids == Set.empty then + noSub ( model, Cmd.none ) + + else + let + ( mm, mc ) = + Comp.PublishItems.initQuery + (Q.ItemIdIn (Set.toList svm.ids)) + in + noSub + ( { model + | viewMode = + SelectView + { svm + | action = PublishSelected + , publishModel = mm + } + } + , Cmd.map PublishItemsMsg mc + ) + + _ -> + noSub ( model, Cmd.none ) + + PublishItemsMsg lmsg -> + case model.viewMode of + SelectView svm -> + let + result = + Comp.PublishItems.update flags lmsg svm.publishModel + + nextView = + case result.outcome of + Comp.PublishItems.OutcomeDone -> + SelectView { svm | action = NoneAction } + + Comp.PublishItems.OutcomeInProgress -> + SelectView { svm | publishModel = result.model } + + model_ = + { model | viewMode = nextView } + in + if result.outcome == Comp.PublishItems.OutcomeDone then + update mId + key + flags + settings + (DoSearch model.searchTypeDropdownValue) + model_ + + else + noSub + ( model_ + , Cmd.map PublishItemsMsg result.cmd + ) + + _ -> + noSub ( model, Cmd.none ) + EditMenuMsg lmsg -> case model.viewMode of SelectView svm -> @@ -786,6 +872,38 @@ update mId key flags settings msg model = RemoveItem id -> update mId key flags settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model + TogglePublishCurrentQueryView -> + case createQuery model of + Just q -> + let + ( pm, pc ) = + Comp.PublishItems.initQuery q + in + noSub ( { model | viewMode = PublishView pm }, Cmd.map PublishViewMsg pc ) + + Nothing -> + noSub ( model, Cmd.none ) + + PublishViewMsg lmsg -> + case model.viewMode of + PublishView inPM -> + let + result = + Comp.PublishItems.update flags lmsg inPM + in + case result.outcome of + Comp.PublishItems.OutcomeInProgress -> + noSub + ( { model | viewMode = PublishView result.model } + , Cmd.map PublishViewMsg result.cmd + ) + + Comp.PublishItems.OutcomeDone -> + noSub ( { model | viewMode = SearchView }, Cmd.none ) + + _ -> + noSub ( model, Cmd.none ) + --- Helpers diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 40dd3b3c..63f39957 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -13,9 +13,12 @@ import Comp.ItemCardList import Comp.ItemMerge import Comp.MenuBar as MB import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Comp.SearchStatsView import Data.Flags exposing (Flags) +import Data.Icons as Icons +import Data.ItemQuery as Q import Data.ItemSelection import Data.SearchMode import Data.UiSettings exposing (UiSettings) @@ -63,29 +66,52 @@ viewContent texts flags settings model = mainView : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) mainView texts flags settings model = let - mergeView = + otherView = case model.viewMode of SelectView svm -> case svm.action of MergeSelected -> - Just svm + Just + [ div [ class "sm:relative mb-2" ] + (itemMergeView texts settings svm) + ] + + PublishSelected -> + Just + [ div [ class "sm:relative mb-2" ] + (itemPublishView texts flags svm) + ] _ -> Nothing - _ -> + PublishView pm -> + Just + [ div [ class "sm:relative mb-2" ] + (publishResults texts flags model pm) + ] + + SimpleView -> + Nothing + + SearchView -> Nothing in - case mergeView of - Just svm -> - [ div [ class "sm:relative mb-2" ] - (itemMergeView texts settings svm) - ] + case otherView of + Just body -> + body Nothing -> itemCardList texts flags settings model +itemPublishView : Texts -> Flags -> SelectViewModel -> List (Html Msg) +itemPublishView texts flags svm = + [ Html.map PublishItemsMsg + (Comp.PublishItems.view texts.publishItems flags svm.publishModel) + ] + + itemMergeView : Texts -> UiSettings -> SelectViewModel -> List (Html Msg) itemMergeView texts settings svm = [ Html.map MergeItemsMsg @@ -93,6 +119,13 @@ itemMergeView texts settings svm = ] +publishResults : Texts -> Flags -> Model -> Comp.PublishItems.Model -> List (Html Msg) +publishResults texts flags model pm = + [ Html.map PublishViewMsg + (Comp.PublishItems.view texts.publishItems flags pm) + ] + + confirmModal : Texts -> Model -> List (Html Msg) confirmModal texts model = let @@ -148,6 +181,9 @@ itemsBar texts flags settings model = SelectView svm -> [ editMenuBar texts model svm ] + PublishView query -> + [ defaultMenuBar texts flags settings model ] + defaultMenuBar : Texts -> Flags -> UiSettings -> Model -> Html Msg defaultMenuBar texts flags settings model = @@ -215,6 +251,25 @@ defaultMenuBar texts flags settings model = MB.view { end = [ MB.CustomElement <| + B.secondaryBasicButton + { label = "" + , icon = Icons.share + , disabled = createQuery model == Nothing + , handler = onClick TogglePublishCurrentQueryView + , attrs = + [ title <| + if createQuery model == Nothing then + texts.nothingSelectedToShare + + else + texts.publishCurrentQueryTitle + , classList + [ ( btnStyle, True ) + ] + , href "#" + ] + } + , MB.CustomElement <| B.secondaryBasicButton { label = "" , icon = @@ -332,6 +387,17 @@ editMenuBar texts model svm = , ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Trashed ) ] } + , MB.CustomButton + { tagger = PublishSelectedItems + , label = "" + , icon = Just Icons.share + , title = texts.publishItemsTitle selectCount + , inputClass = + [ ( btnStyle, True ) + , ( "bg-gray-200 dark:bg-bluegray-600", svm.action == PublishSelected ) + , ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Trashed ) + ] + } ] , end = [ MB.CustomButton @@ -413,12 +479,12 @@ itemCardList texts _ settings model = settings model.itemListModel ) - , loadMore settings model + , loadMore texts settings model ] -loadMore : UiSettings -> Model -> Html Msg -loadMore settings model = +loadMore : Texts -> UiSettings -> Model -> Html Msg +loadMore texts settings model = let inactive = not model.moreAvailable || model.moreInProgress || model.searchInProgress @@ -430,10 +496,10 @@ loadMore settings model = [ B.secondaryBasicButton { label = if model.moreAvailable then - "Load more…" + texts.loadMore else - "That's all" + texts.thatsAll , icon = if model.moreInProgress then "fa fa-circle-notch animate-spin" diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index c7686ffa..f33ba301 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -48,6 +48,11 @@ errorMessage = " border border-red-600 bg-red-50 text-red-600 dark:border-orange-800 dark:bg-orange-300 dark:text-orange-800 px-2 py-2 rounded " +errorText : String +errorText = + " text-red-600 dark:text-orange-800 " + + warnMessage : String warnMessage = warnMessageColors ++ " border dark:bg-opacity-25 px-2 py-2 rounded "