mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-23 10:58:26 +00:00
Save and load dashboards
This commit is contained in:
@ -144,8 +144,6 @@ titleDiv : String -> Html msg
|
||||
titleDiv label =
|
||||
div [ class "text-sm opacity-75 py-0.5 italic" ]
|
||||
[ text label
|
||||
|
||||
--, text " ──"
|
||||
]
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
|
312
modules/webapp/src/main/elm/Comp/DashboardManage.elm
Normal file
312
modules/webapp/src/main/elm/Comp/DashboardManage.elm
Normal 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 ""
|
@ -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
|
||||
|
Reference in New Issue
Block a user