Save and load dashboards

This commit is contained in:
eikek
2022-01-26 21:23:48 +01:00
parent e83bf6b750
commit 3ff7e255b4
24 changed files with 1647 additions and 192 deletions

View File

@ -144,8 +144,6 @@ titleDiv : String -> Html msg
titleDiv label =
div [ class "text-sm opacity-75 py-0.5 italic" ]
[ text label
--, text " ──"
]

View File

@ -1,30 +1,34 @@
module Comp.DashboardEdit exposing (Model, Msg, SubmitAction(..), init, update, view, viewBox)
module Comp.DashboardEdit exposing (Model, Msg, getBoard, init, update, view, viewBox)
import Comp.Basic as B
import Comp.BoxEdit
import Comp.FixedDropdown
import Comp.MenuBar as MB
import Data.AccountScope exposing (AccountScope)
import Data.Box exposing (Box)
import Data.Dashboard exposing (Dashboard)
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Dict exposing (Dict)
import Html exposing (Html, div, i, input, label, text)
import Html.Attributes exposing (class, classList, href, placeholder, type_, value)
import Html.Events exposing (onClick, onInput)
import Html exposing (Html, div, i, input, label, span, text)
import Html.Attributes exposing (checked, class, classList, href, placeholder, type_, value)
import Html.Events exposing (onCheck, onClick, onInput)
import Html5.DragDrop as DD
import Messages.Comp.DashboardEdit exposing (Texts)
import Styles as S
import Util.Maybe
type alias Model =
{ dashboard : Dashboard
, originalName : String
, boxModels : Dict Int Comp.BoxEdit.Model
, nameValue : Maybe String
, nameValue : String
, columnsModel : Comp.FixedDropdown.Model Int
, columnsValue : Maybe Int
, gapModel : Comp.FixedDropdown.Model Int
, gapValue : Maybe Int
, defaultDashboard : Bool
, scope : AccountScope
, newBoxMenuOpen : Bool
, boxDragDrop : DD.Model Int Int
}
@ -32,25 +36,18 @@ type alias Model =
type Msg
= BoxMsg Int Comp.BoxEdit.Msg
| SaveDashboard
| Cancel
| RequestDelete
| SetName String
| ColumnsMsg (Comp.FixedDropdown.Msg Int)
| GapMsg (Comp.FixedDropdown.Msg Int)
| ToggleNewBoxMenu
| SetScope AccountScope
| ToggleDefault
| PrependNew Box
| DragDropMsg (DD.Msg Int Int)
type SubmitAction
= SubmitSave Dashboard
| SubmitCancel
| SubmitDelete String
| SubmitNone
init : Flags -> Dashboard -> ( Model, Cmd Msg, Sub Msg )
init flags db =
init : Flags -> Dashboard -> AccountScope -> Bool -> ( Model, Cmd Msg, Sub Msg )
init flags db scope default =
let
( boxModels, cmdsAndSubs ) =
List.map (Comp.BoxEdit.init flags) db.boxes
@ -65,10 +62,13 @@ init flags db =
List.unzip cmdsAndSubs
in
( { dashboard = db
, originalName = db.name
, nameValue = Just db.name
, nameValue = db.name
, columnsModel = Comp.FixedDropdown.init [ 1, 2, 3, 4, 5 ]
, columnsValue = Just db.columns
, gapModel = Comp.FixedDropdown.init (List.range 0 12)
, gapValue = Just db.gap
, defaultDashboard = default
, scope = scope
, newBoxMenuOpen = False
, boxModels =
List.indexedMap Tuple.pair boxModels
@ -80,6 +80,11 @@ init flags db =
)
getBoard : Model -> ( Dashboard, AccountScope, Bool )
getBoard model =
( model.dashboard, model.scope, model.defaultDashboard )
--- Update
@ -88,7 +93,6 @@ type alias UpdateResult =
{ model : Model
, cmd : Cmd Msg
, sub : Sub Msg
, action : SubmitAction
}
@ -115,26 +119,20 @@ update flags msg model =
{ model = { model | boxModels = newBoxes, dashboard = db_ }
, cmd = Cmd.map (BoxMsg index) result.cmd
, sub = Sub.map (BoxMsg index) result.sub
, action = SubmitNone
}
Nothing ->
unit model
SetName str ->
case Util.Maybe.fromString str of
Just s ->
let
db =
model.dashboard
let
db =
model.dashboard
db_ =
{ db | name = s }
in
unit { model | dashboard = db_, nameValue = Just s }
Nothing ->
unit { model | nameValue = Nothing }
db_ =
{ db | name = String.trim str }
in
unit { model | dashboard = db_, nameValue = str }
ColumnsMsg lm ->
let
@ -149,14 +147,18 @@ update flags msg model =
in
unit { model | columnsValue = value, columnsModel = cm, dashboard = db_ }
SaveDashboard ->
UpdateResult model Cmd.none Sub.none (SubmitSave model.dashboard)
GapMsg lm ->
let
( gm, value ) =
Comp.FixedDropdown.update lm model.gapModel
Cancel ->
UpdateResult model Cmd.none Sub.none SubmitCancel
db =
model.dashboard
RequestDelete ->
UpdateResult model Cmd.none Sub.none (SubmitDelete model.originalName)
db_ =
{ db | gap = Maybe.withDefault db.gap value }
in
unit { model | gapModel = gm, gapValue = value, dashboard = db_ }
ToggleNewBoxMenu ->
unit { model | newBoxMenuOpen = not model.newBoxMenuOpen }
@ -186,7 +188,6 @@ update flags msg model =
{ model = { model | boxModels = newBoxes, dashboard = db_, newBoxMenuOpen = False }
, cmd = Cmd.map (BoxMsg index) bc
, sub = Sub.map (BoxMsg index) bs
, action = SubmitNone
}
DragDropMsg lm ->
@ -207,10 +208,16 @@ update flags msg model =
in
unit nextModel
SetScope s ->
unit { model | scope = s }
ToggleDefault ->
unit { model | defaultDashboard = not model.defaultDashboard }
unit : Model -> UpdateResult
unit model =
UpdateResult model Cmd.none Sub.none SubmitNone
UpdateResult model Cmd.none Sub.none
applyBoxAction :
@ -365,34 +372,18 @@ viewMain texts _ _ model =
}
in
div [ class "my-2 " ]
[ MB.view
{ start =
[ MB.PrimaryButton
{ tagger = SaveDashboard
, title = texts.basics.submitThisForm
, icon = Just "fa fa-save"
, label = texts.basics.submit
}
, MB.SecondaryButton
{ tagger = Cancel
, title = texts.basics.cancel
, icon = Just "fa fa-times"
, label = texts.basics.cancel
}
]
, end = []
, rootClasses = ""
}
, div [ class "flex flex-col" ]
[ div [ class "flex flex-col" ]
[ div [ class "mt-2" ]
[ label [ class S.inputLabel ]
[ text texts.basics.name
, B.inputRequired
]
, input
[ type_ "text"
, placeholder texts.namePlaceholder
, class S.textInput
, value (Maybe.withDefault "" model.nameValue)
, classList [ ( S.inputErrorBorder, String.trim model.nameValue == "" ) ]
, value model.nameValue
, onInput SetName
]
[]
@ -401,13 +392,58 @@ viewMain texts _ _ model =
[ label [ class S.inputLabel ]
[ text texts.columns
]
, Html.map ColumnsMsg
(Comp.FixedDropdown.viewStyled2 columnsSettings
False
model.columnsValue
model.columnsModel
)
]
, div [ class "mt-2" ]
[ label [ class S.inputLabel ]
[ text texts.gap
]
, Html.map GapMsg
(Comp.FixedDropdown.viewStyled2 columnsSettings
False
model.gapValue
model.gapModel
)
]
, div [ class "mt-2" ]
[ div [ class "flex flex-row space-x-4" ]
[ label [ class "inline-flex items-center" ]
[ input
[ type_ "radio"
, checked (Data.AccountScope.isUser model.scope)
, onCheck (\_ -> SetScope Data.AccountScope.User)
, class S.radioInput
]
[]
, span [ class "ml-2" ] [ text <| texts.accountScope Data.AccountScope.User ]
]
, label [ class "inline-flex items-center" ]
[ input
[ type_ "radio"
, checked (Data.AccountScope.isCollective model.scope)
, onCheck (\_ -> SetScope Data.AccountScope.Collective)
, class S.radioInput
]
[]
, span [ class "ml-2" ]
[ text <| texts.accountScope Data.AccountScope.Collective ]
]
]
]
, div [ class "mt-2" ]
[ MB.viewItem <|
MB.Checkbox
{ tagger = \_ -> ToggleDefault
, label = texts.defaultDashboard
, id = ""
, value = model.defaultDashboard
}
]
, Html.map ColumnsMsg
(Comp.FixedDropdown.viewStyled2 columnsSettings
False
model.columnsValue
model.columnsModel
)
]
]

View File

@ -0,0 +1,312 @@
module Comp.DashboardManage exposing (Model, Msg, SubmitAction(..), UpdateResult, init, update, view)
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Comp.Basic as B
import Comp.DashboardEdit
import Comp.MenuBar as MB
import Data.AccountScope exposing (AccountScope)
import Data.Dashboard exposing (Dashboard)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (Html, div, i, text)
import Html.Attributes exposing (class, classList)
import Http
import Messages.Comp.DashboardManage exposing (Texts)
import Styles as S
type alias Model =
{ edit : Comp.DashboardEdit.Model
, initData : InitData
, deleteRequested : Bool
, formError : Maybe FormError
}
type Msg
= SaveDashboard
| Cancel
| DeleteDashboard
| SetRequestDelete Bool
| EditMsg Comp.DashboardEdit.Msg
| DeleteResp (Result Http.Error BasicResult)
| SaveResp String (Result Http.Error BasicResult)
| CreateNew
| CopyCurrent
type FormError
= FormInvalid String
| FormHttpError Http.Error
| FormNameEmpty
| FormNameExists
type alias InitData =
{ flags : Flags
, dashboard : Dashboard
, scope : AccountScope
, isDefault : Bool
}
init : InitData -> ( Model, Cmd Msg, Sub Msg )
init data =
let
( em, ec, es ) =
Comp.DashboardEdit.init data.flags data.dashboard data.scope data.isDefault
model =
{ edit = em
, initData = data
, deleteRequested = False
, formError = Nothing
}
in
( model, Cmd.map EditMsg ec, Sub.map EditMsg es )
--- Update
type SubmitAction
= SubmitNone
| SubmitCancel String
| SubmitSaved String
| SubmitDeleted
type alias UpdateResult =
{ model : Model
, cmd : Cmd Msg
, sub : Sub Msg
, action : SubmitAction
}
update : Flags -> (String -> Bool) -> Msg -> Model -> UpdateResult
update flags nameExists msg model =
case msg of
EditMsg lm ->
let
result =
Comp.DashboardEdit.update flags lm model.edit
in
{ model = { model | edit = result.model }
, cmd = Cmd.map EditMsg result.cmd
, sub = Sub.map EditMsg result.sub
, action = SubmitNone
}
CreateNew ->
let
initData =
{ flags = flags
, dashboard = Data.Dashboard.empty
, scope = Data.AccountScope.User
, isDefault = False
}
( m, c, s ) =
init initData
in
UpdateResult m c s SubmitNone
CopyCurrent ->
let
( current, scope, isDefault ) =
Comp.DashboardEdit.getBoard model.edit
initData =
{ flags = flags
, dashboard = { current | name = "" }
, scope = scope
, isDefault = isDefault
}
( m, c, s ) =
init initData
in
UpdateResult m c s SubmitNone
SetRequestDelete flag ->
unit { model | deleteRequested = flag }
SaveDashboard ->
let
( tosave, scope, isDefault ) =
Comp.DashboardEdit.getBoard model.edit
saveCmd =
Api.replaceDashboard flags
model.initData.dashboard.name
tosave
scope
isDefault
(SaveResp tosave.name)
in
if tosave.name == "" then
unit { model | formError = Just FormNameEmpty }
else if tosave.name /= model.initData.dashboard.name && nameExists tosave.name then
unit { model | formError = Just FormNameExists }
else
UpdateResult model saveCmd Sub.none SubmitNone
Cancel ->
unitAction model (SubmitCancel model.initData.dashboard.name)
DeleteDashboard ->
let
deleteCmd =
Api.deleteDashboard flags model.initData.dashboard.name model.initData.scope DeleteResp
in
UpdateResult model deleteCmd Sub.none SubmitNone
SaveResp name (Ok result) ->
if result.success then
unitAction model (SubmitSaved name)
else
unit { model | formError = Just (FormInvalid result.message) }
SaveResp _ (Err err) ->
unit { model | formError = Just (FormHttpError err) }
DeleteResp (Ok result) ->
if result.success then
unitAction model SubmitDeleted
else
unit { model | formError = Just (FormInvalid result.message) }
DeleteResp (Err err) ->
unit { model | formError = Just (FormHttpError err) }
unit : Model -> UpdateResult
unit model =
UpdateResult model Cmd.none Sub.none SubmitNone
unitAction : Model -> SubmitAction -> UpdateResult
unitAction model action =
UpdateResult model Cmd.none Sub.none action
--- View
type alias ViewSettings =
{ showDeleteButton : Bool
, showCopyButton : Bool
}
view : Texts -> Flags -> ViewSettings -> UiSettings -> Model -> Html Msg
view texts flags cfg settings model =
div []
[ B.contentDimmer model.deleteRequested
(div [ class "flex flex-col" ]
[ div [ class "text-xl" ]
[ i [ class "fa fa-info-circle mr-2" ] []
, text texts.reallyDeleteDashboard
]
, div [ class "mt-4 flex flex-row items-center space-x-2" ]
[ MB.viewItem <|
MB.DeleteButton
{ tagger = DeleteDashboard
, title = ""
, label = texts.basics.yes
, icon = Just "fa fa-check"
}
, MB.viewItem <|
MB.SecondaryButton
{ tagger = SetRequestDelete False
, title = ""
, label = texts.basics.no
, icon = Just "fa fa-times"
}
]
]
)
, MB.view
{ start =
[ MB.PrimaryButton
{ tagger = SaveDashboard
, title = texts.basics.submitThisForm
, icon = Just "fa fa-save"
, label = texts.basics.submit
}
, MB.SecondaryButton
{ tagger = Cancel
, title = texts.basics.cancel
, icon = Just "fa fa-times"
, label = texts.basics.cancel
}
]
, end =
[ MB.BasicButton
{ tagger = CreateNew
, title = texts.createDashboard
, icon = Just "fa fa-plus"
, label = texts.createDashboard
}
, MB.CustomButton
{ tagger = CopyCurrent
, title = texts.copyDashboard
, icon = Just "fa fa-copy"
, label = texts.copyDashboard
, inputClass =
[ ( S.secondaryBasicButton, True )
, ( "hidden", not cfg.showCopyButton )
]
}
, MB.CustomButton
{ tagger = SetRequestDelete True
, title = texts.basics.delete
, icon = Just "fa fa-times"
, label = texts.basics.delete
, inputClass =
[ ( S.deleteButton, True )
, ( "hidden", not cfg.showDeleteButton )
]
}
]
, rootClasses = ""
}
, div
[ class S.errorMessage
, class "mt-2"
, classList [ ( "hidden", model.formError == Nothing ) ]
]
[ errorMessage texts model
]
, div []
[ Html.map EditMsg
(Comp.DashboardEdit.view texts.dashboardEdit flags settings model.edit)
]
]
errorMessage : Texts -> Model -> Html Msg
errorMessage texts model =
case model.formError of
Just (FormInvalid errMsg) ->
text errMsg
Just (FormHttpError err) ->
text (texts.httpError err)
Just FormNameEmpty ->
text texts.nameEmpty
Just FormNameExists ->
text texts.nameExists
Nothing ->
text ""

View File

@ -103,24 +103,28 @@ viewBox texts flags settings index box =
--- Helpers
{-| note due to tailwinds purging css that is not found in source
files, need to spell them out somewhere - which is done it keep.txt in
this case.
-}
gridStyle : Dashboard -> String
gridStyle db =
let
cappedGap =
min db.gap 12
cappedCol =
min db.columns 12
gapStyle =
" gap-" ++ String.fromInt cappedGap ++ " "
colStyle =
case db.columns of
1 ->
""
2 ->
"md:grid-cols-2"
3 ->
"md:grid-cols-3"
4 ->
"md:grid-cols-4"
_ ->
"md:grid-cols-5"
" md:grid-cols-" ++ String.fromInt cappedCol ++ " "
in
"grid gap-4 grid-cols-1 " ++ colStyle
"grid grid-cols-1 " ++ gapStyle ++ colStyle