Create shares from search and select view

This commit is contained in:
eikek
2021-10-02 23:46:58 +02:00
parent 189009325e
commit aa21e7a74c
14 changed files with 1046 additions and 131 deletions

View File

@ -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
]
]

View File

@ -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." ]
]
]

View File

@ -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
]

View File

@ -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

View File

@ -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)