Add date picker to silence form views (#2262)
* add datepicker Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * fix import error Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * fix unnecessary import from DateTime package Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * fix unnecessary import utc Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * change datetime picker component Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * added datetime picker utils Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * added datetime picker utils Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * remove config Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com> * replace case expressions to Result.toMaybe Signed-off-by: m-masataka <m.mizukoshi.wakuwaku@gmail.com>
This commit is contained in:
parent
277c9ed462
commit
73db78741f
File diff suppressed because one or more lines are too long
|
@ -16,11 +16,13 @@
|
|||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.1.2",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0"
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"justinmimbs/time-extra": "1.1.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/virtual-dom": "1.0.0",
|
||||
"elm/random": "1.0.0"
|
||||
"elm/random": "1.0.0",
|
||||
"justinmimbs/date": "3.2.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
.cursor-pointer {cursor: pointer;}
|
||||
|
||||
.month {
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.calendar_ .date-container {
|
||||
}
|
||||
|
||||
.calendar_ .weekheader {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.calendar_ .date {
|
||||
color: #C0C0C0;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
width: 40px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: .75rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.calendar_ .date.thismonth {
|
||||
color: #22292f;
|
||||
}
|
||||
|
||||
.calendar_ .date.front {
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.calendar_ .date.back {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.calendar_ .date.front.mouseover {
|
||||
background-color: #EEEEEE;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.calendar_ .date.front.start {
|
||||
background-color: #b0c4de;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.calendar_ .date.front.end {
|
||||
background-color: #b0c4de;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.calendar_ .date.back.start {
|
||||
background-color: #b0c4de;
|
||||
border-top-left-radius: 50%;
|
||||
border-bottom-left-radius: 50%;
|
||||
}
|
||||
|
||||
.calendar_ .date.back.end {
|
||||
background-color: #b0c4de;
|
||||
border-top-right-radius: 50%;
|
||||
border-bottom-right-radius: 50%;
|
||||
}
|
||||
|
||||
.calendar_ .date.back.between {
|
||||
background-color: #b0c4de;
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
height:60px;
|
||||
width:80%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.timepicker .subject {
|
||||
padding:10px;
|
||||
width:20%;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
.timepicker .hour {
|
||||
width:10%;
|
||||
}
|
||||
|
||||
.timepicker .minute {
|
||||
width:10%;
|
||||
}
|
||||
|
||||
.timepicker .view {
|
||||
width:100%;
|
||||
height:50%;
|
||||
text-align:center;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
.timepicker .up-button {
|
||||
width:100%;
|
||||
height:25%;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
.timepicker .down-button {
|
||||
width:100%;
|
||||
height:25%;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
.timepicker .colon {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.timepicker .timeview {
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.month-header {
|
||||
width:70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.month-header .prev-month {
|
||||
width:20%;
|
||||
}
|
||||
|
||||
.month-header .month-text {
|
||||
width:60%;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.month-header .next-month {
|
||||
width:20%;
|
||||
}
|
||||
|
||||
.d-flex-center {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
|
@ -101,6 +101,7 @@ init flags url key =
|
|||
libUrl
|
||||
Loading
|
||||
Loading
|
||||
Loading
|
||||
defaultCreator
|
||||
groupExpandAll
|
||||
key
|
||||
|
|
|
@ -23,6 +23,7 @@ type alias Model =
|
|||
, libUrl : String
|
||||
, bootstrapCSS : ApiData String
|
||||
, fontAwesomeCSS : ApiData String
|
||||
, elmDatepickerCSS : ApiData String
|
||||
, defaultCreator : String
|
||||
, expandAll : Bool
|
||||
, key : Key
|
||||
|
@ -49,6 +50,7 @@ type Msg
|
|||
| UpdateFilter String
|
||||
| BootstrapCSSLoaded (ApiData String)
|
||||
| FontAwesomeCSSLoaded (ApiData String)
|
||||
| ElmDatepickerCSSLoaded (ApiData String)
|
||||
| SetDefaultCreator String
|
||||
| SetGroupExpandAll Bool
|
||||
|
||||
|
|
|
@ -121,6 +121,9 @@ update msg ({ basePath, apiUrl } as model) =
|
|||
FontAwesomeCSSLoaded css ->
|
||||
( { model | fontAwesomeCSS = css }, Cmd.none )
|
||||
|
||||
ElmDatepickerCSSLoaded css ->
|
||||
( { model | elmDatepickerCSS = css }, Cmd.none )
|
||||
|
||||
SetDefaultCreator name ->
|
||||
( { model | defaultCreator = name }, Cmd.none )
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
module Utils.DateTimePicker.Types exposing
|
||||
( DateTimePicker
|
||||
, InputHourOrMinute(..)
|
||||
, Msg(..)
|
||||
, StartOrEnd(..)
|
||||
, initDateTimePicker
|
||||
, initFromStartAndEndTime
|
||||
)
|
||||
|
||||
import Time exposing (Posix)
|
||||
import Utils.DateTimePicker.Utils exposing (floorMinute)
|
||||
|
||||
|
||||
type alias DateTimePicker =
|
||||
{ month : Maybe Posix
|
||||
, mouseOverDay : Maybe Posix
|
||||
, startDate : Maybe Posix
|
||||
, endDate : Maybe Posix
|
||||
, startTime : Maybe Posix
|
||||
, endTime : Maybe Posix
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= NextMonth
|
||||
| PrevMonth
|
||||
| MouseOverDay Posix
|
||||
| OnClickDay
|
||||
| ClearMouseOverDay
|
||||
| SetInputTime StartOrEnd InputHourOrMinute Int
|
||||
| IncrementTime StartOrEnd InputHourOrMinute Int
|
||||
|
||||
|
||||
type StartOrEnd
|
||||
= Start
|
||||
| End
|
||||
|
||||
|
||||
type InputHourOrMinute
|
||||
= InputHour
|
||||
| InputMinute
|
||||
|
||||
|
||||
initDateTimePicker : DateTimePicker
|
||||
initDateTimePicker =
|
||||
{ month = Nothing
|
||||
, mouseOverDay = Nothing
|
||||
, startDate = Nothing
|
||||
, endDate = Nothing
|
||||
, startTime = Nothing
|
||||
, endTime = Nothing
|
||||
}
|
||||
|
||||
|
||||
initFromStartAndEndTime : Maybe Posix -> Maybe Posix -> DateTimePicker
|
||||
initFromStartAndEndTime start end =
|
||||
let
|
||||
startTime =
|
||||
Maybe.map (\s -> floorMinute s) start
|
||||
|
||||
endTime =
|
||||
Maybe.map (\e -> floorMinute e) end
|
||||
in
|
||||
{ month = start
|
||||
, mouseOverDay = Nothing
|
||||
, startDate = start
|
||||
, endDate = end
|
||||
, startTime = startTime
|
||||
, endTime = endTime
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
module Utils.DateTimePicker.Updates exposing (update)
|
||||
|
||||
import Time exposing (Posix)
|
||||
import Utils.DateTimePicker.Types
|
||||
exposing
|
||||
( DateTimePicker
|
||||
, InputHourOrMinute(..)
|
||||
, Msg(..)
|
||||
, StartOrEnd(..)
|
||||
)
|
||||
import Utils.DateTimePicker.Utils
|
||||
exposing
|
||||
( addHour
|
||||
, addMinute
|
||||
, firstDayOfNextMonth
|
||||
, firstDayOfPrevMonth
|
||||
, floorDate
|
||||
, trimTime
|
||||
, updateHour
|
||||
, updateMinute
|
||||
)
|
||||
|
||||
|
||||
update : Msg -> DateTimePicker -> DateTimePicker
|
||||
update msg dateTimePicker =
|
||||
let
|
||||
justMonth =
|
||||
dateTimePicker.month
|
||||
|> Maybe.withDefault (Time.millisToPosix 0)
|
||||
|
||||
setTime_ : StartOrEnd -> InputHourOrMinute -> (InputHourOrMinute -> Posix -> Posix) -> ( Maybe Posix, Maybe Posix )
|
||||
setTime_ soe ihom updateTime =
|
||||
let
|
||||
set_ : Maybe Posix -> Maybe Posix
|
||||
set_ a =
|
||||
Maybe.map (\b -> updateTime ihom b) a
|
||||
in
|
||||
case soe of
|
||||
Start ->
|
||||
( set_ dateTimePicker.startTime, dateTimePicker.endTime )
|
||||
|
||||
End ->
|
||||
( dateTimePicker.startTime, set_ dateTimePicker.endTime )
|
||||
in
|
||||
case msg of
|
||||
NextMonth ->
|
||||
{ dateTimePicker | month = Just (firstDayOfNextMonth justMonth) }
|
||||
|
||||
PrevMonth ->
|
||||
{ dateTimePicker | month = Just (firstDayOfPrevMonth justMonth) }
|
||||
|
||||
MouseOverDay time ->
|
||||
{ dateTimePicker | mouseOverDay = Just time }
|
||||
|
||||
ClearMouseOverDay ->
|
||||
{ dateTimePicker | mouseOverDay = Nothing }
|
||||
|
||||
OnClickDay ->
|
||||
let
|
||||
addDateTime_ : Posix -> Maybe Posix -> Posix
|
||||
addDateTime_ date maybeTime =
|
||||
case maybeTime of
|
||||
Just time ->
|
||||
floorDate date
|
||||
|> Time.posixToMillis
|
||||
|> (\d ->
|
||||
trimTime time
|
||||
|> Time.posixToMillis
|
||||
|> (\t -> d + t)
|
||||
)
|
||||
|> Time.millisToPosix
|
||||
|
||||
Nothing ->
|
||||
floorDate date
|
||||
|
||||
updateTime_ : Maybe Posix -> Maybe Posix -> Maybe Posix
|
||||
updateTime_ maybeDate maybeTime =
|
||||
case maybeDate of
|
||||
Just date ->
|
||||
Just <| addDateTime_ date maybeTime
|
||||
|
||||
Nothing ->
|
||||
maybeTime
|
||||
|
||||
( startDate, endDate ) =
|
||||
case dateTimePicker.mouseOverDay of
|
||||
Just m ->
|
||||
case ( dateTimePicker.startDate, dateTimePicker.endDate ) of
|
||||
( Nothing, Nothing ) ->
|
||||
( Just m
|
||||
, Nothing
|
||||
)
|
||||
|
||||
( Just start, Nothing ) ->
|
||||
case
|
||||
compare (floorDate m |> Time.posixToMillis)
|
||||
(floorDate start |> Time.posixToMillis)
|
||||
of
|
||||
LT ->
|
||||
( Just m
|
||||
, Just start
|
||||
)
|
||||
|
||||
_ ->
|
||||
( Just start
|
||||
, Just m
|
||||
)
|
||||
|
||||
( Nothing, Just end ) ->
|
||||
( Just m
|
||||
, Just end
|
||||
)
|
||||
|
||||
( Just start, Just end ) ->
|
||||
( Just m
|
||||
, Nothing
|
||||
)
|
||||
|
||||
_ ->
|
||||
( dateTimePicker.startDate
|
||||
, dateTimePicker.endDate
|
||||
)
|
||||
in
|
||||
{ dateTimePicker
|
||||
| startDate = startDate
|
||||
, endDate = endDate
|
||||
, startTime = updateTime_ startDate dateTimePicker.startTime
|
||||
, endTime = updateTime_ endDate dateTimePicker.endTime
|
||||
}
|
||||
|
||||
SetInputTime startOrEnd inputHourOrMinute num ->
|
||||
let
|
||||
limit_ : Int -> Int -> Int
|
||||
limit_ limit n =
|
||||
if n < 0 then
|
||||
0
|
||||
|
||||
else
|
||||
modBy limit n
|
||||
|
||||
updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
|
||||
updateHourOrMinute_ ihom s =
|
||||
case ihom of
|
||||
InputHour ->
|
||||
updateHour (limit_ 24 num) s
|
||||
|
||||
InputMinute ->
|
||||
updateMinute (limit_ 60 num) s
|
||||
|
||||
( startTime, endTime ) =
|
||||
setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
|
||||
in
|
||||
{ dateTimePicker | startTime = startTime, endTime = endTime }
|
||||
|
||||
IncrementTime startOrEnd inputHourOrMinute num ->
|
||||
let
|
||||
updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
|
||||
updateHourOrMinute_ ihom s =
|
||||
let
|
||||
compare_ : Posix -> Posix
|
||||
compare_ a =
|
||||
if
|
||||
(floorDate s |> Time.posixToMillis)
|
||||
== (floorDate a |> Time.posixToMillis)
|
||||
then
|
||||
a
|
||||
|
||||
else
|
||||
s
|
||||
in
|
||||
case ihom of
|
||||
InputHour ->
|
||||
addHour num s
|
||||
|> compare_
|
||||
|
||||
InputMinute ->
|
||||
addMinute num s
|
||||
|> compare_
|
||||
|
||||
( startTime, endTime ) =
|
||||
setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
|
||||
in
|
||||
{ dateTimePicker | startTime = startTime, endTime = endTime }
|
|
@ -0,0 +1,217 @@
|
|||
module Utils.DateTimePicker.Utils exposing
|
||||
( addHour
|
||||
, addMinute
|
||||
, firstDayOfNextMonth
|
||||
, firstDayOfPrevMonth
|
||||
, floorDate
|
||||
, floorMinute
|
||||
, floorMonth
|
||||
, listDaysOfMonth
|
||||
, monthToString
|
||||
, splitWeek
|
||||
, targetValueIntParse
|
||||
, trimTime
|
||||
, updateHour
|
||||
, updateMinute
|
||||
)
|
||||
|
||||
import Html.Events exposing (targetValue)
|
||||
import Json.Decode as Decode
|
||||
import Time exposing (Month(..), Posix, Weekday(..), Zone, utc)
|
||||
import Time.Extra as Time exposing (Interval(..))
|
||||
|
||||
|
||||
listDaysOfMonth : Posix -> List Posix
|
||||
listDaysOfMonth time =
|
||||
let
|
||||
firstOfMonth =
|
||||
Time.floor Time.Month utc time
|
||||
|
||||
firstOfNextMonth =
|
||||
firstDayOfNextMonth time
|
||||
|
||||
padFront =
|
||||
weekToInt (Time.toWeekday utc firstOfMonth)
|
||||
|> (\wd ->
|
||||
if wd == 7 then
|
||||
0
|
||||
|
||||
else
|
||||
wd
|
||||
)
|
||||
|> (\w -> Time.add Time.Day -w utc firstOfMonth)
|
||||
|> (\d -> Time.range Time.Day 1 utc d firstOfMonth)
|
||||
|
||||
padBack =
|
||||
weekToInt (Time.toWeekday utc firstOfNextMonth)
|
||||
|> (\w -> Time.add Time.Day (7 - w) utc firstOfNextMonth)
|
||||
|> Time.range Time.Day 1 utc firstOfNextMonth
|
||||
in
|
||||
Time.range Time.Day 1 utc firstOfMonth firstOfNextMonth
|
||||
|> (\m -> padFront ++ m ++ padBack)
|
||||
|
||||
|
||||
firstDayOfNextMonth : Posix -> Posix
|
||||
firstDayOfNextMonth time =
|
||||
Time.floor Time.Month utc time
|
||||
|> Time.add Time.Day 1 utc
|
||||
|> Time.ceiling Time.Month utc
|
||||
|
||||
|
||||
firstDayOfPrevMonth : Posix -> Posix
|
||||
firstDayOfPrevMonth time =
|
||||
Time.floor Time.Month utc time
|
||||
|> Time.add Time.Day -1 utc
|
||||
|> Time.floor Time.Month utc
|
||||
|
||||
|
||||
splitWeek : List Posix -> List (List Posix) -> List (List Posix)
|
||||
splitWeek days weeks =
|
||||
if List.length days < 7 then
|
||||
weeks
|
||||
|
||||
else
|
||||
List.append weeks [ List.take 7 days ]
|
||||
|> splitWeek (List.drop 7 days)
|
||||
|
||||
|
||||
floorDate : Posix -> Posix
|
||||
floorDate time =
|
||||
Time.floor Time.Day utc time
|
||||
|
||||
|
||||
floorMonth : Posix -> Posix
|
||||
floorMonth time =
|
||||
Time.floor Time.Month utc time
|
||||
|
||||
|
||||
floorMinute : Posix -> Posix
|
||||
floorMinute time =
|
||||
Time.floor Time.Minute utc time
|
||||
|
||||
|
||||
trimTime : Posix -> Posix
|
||||
trimTime time =
|
||||
Time.floor Time.Day utc time
|
||||
|> Time.posixToMillis
|
||||
|> (\d ->
|
||||
Time.posixToMillis time - d
|
||||
)
|
||||
|> Time.millisToPosix
|
||||
|
||||
|
||||
updateHour : Int -> Posix -> Posix
|
||||
updateHour n time =
|
||||
let
|
||||
diff =
|
||||
n - Time.toHour utc time
|
||||
in
|
||||
Time.add Hour diff utc time
|
||||
|
||||
|
||||
updateMinute : Int -> Posix -> Posix
|
||||
updateMinute n time =
|
||||
let
|
||||
diff =
|
||||
n - Time.toMinute utc time
|
||||
in
|
||||
Time.add Minute diff utc time
|
||||
|
||||
|
||||
addHour : Int -> Posix -> Posix
|
||||
addHour n time =
|
||||
Time.add Hour n utc time
|
||||
|
||||
|
||||
addMinute : Int -> Posix -> Posix
|
||||
addMinute n time =
|
||||
Time.add Minute n utc time
|
||||
|
||||
|
||||
weekToInt : Weekday -> Int
|
||||
weekToInt weekday =
|
||||
case weekday of
|
||||
Mon ->
|
||||
1
|
||||
|
||||
Tue ->
|
||||
2
|
||||
|
||||
Wed ->
|
||||
3
|
||||
|
||||
Thu ->
|
||||
4
|
||||
|
||||
Fri ->
|
||||
5
|
||||
|
||||
Sat ->
|
||||
6
|
||||
|
||||
Sun ->
|
||||
7
|
||||
|
||||
|
||||
monthToString : Month -> String
|
||||
monthToString month =
|
||||
case month of
|
||||
Jan ->
|
||||
"January"
|
||||
|
||||
Feb ->
|
||||
"February"
|
||||
|
||||
Mar ->
|
||||
"March"
|
||||
|
||||
Apr ->
|
||||
"April"
|
||||
|
||||
May ->
|
||||
"May"
|
||||
|
||||
Jun ->
|
||||
"June"
|
||||
|
||||
Jul ->
|
||||
"July"
|
||||
|
||||
Aug ->
|
||||
"August"
|
||||
|
||||
Sep ->
|
||||
"September"
|
||||
|
||||
Oct ->
|
||||
"October"
|
||||
|
||||
Nov ->
|
||||
"November"
|
||||
|
||||
Dec ->
|
||||
"December"
|
||||
|
||||
|
||||
targetValueIntParse : Decode.Decoder Int
|
||||
targetValueIntParse =
|
||||
customDecoder targetValue (String.toInt >> maybeStringToResult)
|
||||
|
||||
|
||||
maybeStringToResult : Maybe a -> Result String a
|
||||
maybeStringToResult =
|
||||
Result.fromMaybe "could not convert string"
|
||||
|
||||
|
||||
customDecoder : Decode.Decoder a -> (a -> Result String b) -> Decode.Decoder b
|
||||
customDecoder d f =
|
||||
let
|
||||
resultDecoder x =
|
||||
case x of
|
||||
Ok a ->
|
||||
Decode.succeed a
|
||||
|
||||
Err e ->
|
||||
Decode.fail e
|
||||
in
|
||||
Decode.map f d |> Decode.andThen resultDecoder
|
|
@ -0,0 +1,305 @@
|
|||
module Utils.DateTimePicker.Views exposing (viewDateTimePicker)
|
||||
|
||||
import Html exposing (Html, br, button, div, i, input, p, strong, text)
|
||||
import Html.Attributes exposing (class, maxlength, value)
|
||||
import Html.Events exposing (on, onClick, onMouseOut, onMouseOver)
|
||||
import Iso8601
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import Time exposing (Posix, utc)
|
||||
import Utils.DateTimePicker.Types exposing (DateTimePicker, InputHourOrMinute(..), Msg(..), StartOrEnd(..))
|
||||
import Utils.DateTimePicker.Updates exposing (update)
|
||||
import Utils.DateTimePicker.Utils
|
||||
exposing
|
||||
( floorDate
|
||||
, floorMonth
|
||||
, listDaysOfMonth
|
||||
, monthToString
|
||||
, splitWeek
|
||||
, targetValueIntParse
|
||||
)
|
||||
|
||||
|
||||
viewDateTimePicker : DateTimePicker -> Html Msg
|
||||
viewDateTimePicker dateTimePicker =
|
||||
div [ class "w-100 container" ]
|
||||
[ viewCalendar dateTimePicker
|
||||
, div [ class "pt-4 row justify-content-center" ]
|
||||
[ viewTimePicker dateTimePicker Start
|
||||
, viewTimePicker dateTimePicker End
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewCalendar : DateTimePicker -> Html Msg
|
||||
viewCalendar dateTimePicker =
|
||||
let
|
||||
justViewTime =
|
||||
dateTimePicker.month
|
||||
|> Maybe.withDefault (Time.millisToPosix 0)
|
||||
in
|
||||
div [ class "calendar_ month" ]
|
||||
[ viewMonthHeader dateTimePicker justViewTime
|
||||
, viewMonth dateTimePicker justViewTime
|
||||
]
|
||||
|
||||
|
||||
viewMonthHeader : DateTimePicker -> Posix -> Html Msg
|
||||
viewMonthHeader dateTimePicker justViewTime =
|
||||
div [ class "row month-header" ]
|
||||
[ div
|
||||
[ class "prev-month d-flex-center"
|
||||
, onClick PrevMonth
|
||||
]
|
||||
[ p
|
||||
[ class "arrow" ]
|
||||
[ i
|
||||
[ class "fa fa-angle-left fa-3x cursor-pointer" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div
|
||||
[ class "month-text d-flex-center" ]
|
||||
[ text (Time.toYear utc justViewTime |> String.fromInt)
|
||||
, br [] []
|
||||
, text (Time.toMonth utc justViewTime |> monthToString)
|
||||
]
|
||||
, div
|
||||
[ class "next-month d-flex-center"
|
||||
, onClick NextMonth
|
||||
]
|
||||
[ p
|
||||
[ class "arrow" ]
|
||||
[ i
|
||||
[ class "fa fa-angle-right fa-3x cursor-pointer" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewMonth : DateTimePicker -> Posix -> Html Msg
|
||||
viewMonth dateTimePicker justViewTime =
|
||||
let
|
||||
days =
|
||||
listDaysOfMonth justViewTime
|
||||
|
||||
weeks =
|
||||
splitWeek days []
|
||||
in
|
||||
div [ class "row justify-content-center" ]
|
||||
[ div [ class "weekheader" ]
|
||||
(List.map viewWeekHeader [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ])
|
||||
, div
|
||||
[ class "date-container"
|
||||
, onMouseOut ClearMouseOverDay
|
||||
]
|
||||
(List.map (viewWeek dateTimePicker justViewTime) weeks)
|
||||
]
|
||||
|
||||
|
||||
viewWeekHeader : String -> Html Msg
|
||||
viewWeekHeader weekday =
|
||||
div [ class "date text-muted" ]
|
||||
[ text weekday ]
|
||||
|
||||
|
||||
viewWeek : DateTimePicker -> Posix -> List Posix -> Html Msg
|
||||
viewWeek dateTimePicker justViewTime days =
|
||||
div []
|
||||
[ div [] (List.map (viewDay dateTimePicker justViewTime) days) ]
|
||||
|
||||
|
||||
viewDay : DateTimePicker -> Posix -> Posix -> Html Msg
|
||||
viewDay dateTimePicker justViewTime day =
|
||||
let
|
||||
compareDate_ : Posix -> Posix -> Order
|
||||
compareDate_ a b =
|
||||
compare (floorDate a |> Time.posixToMillis)
|
||||
(floorDate b |> Time.posixToMillis)
|
||||
|
||||
setClass_ : Maybe Posix -> String -> String
|
||||
setClass_ d s =
|
||||
case d of
|
||||
Just m ->
|
||||
case compareDate_ m day of
|
||||
EQ ->
|
||||
s
|
||||
|
||||
_ ->
|
||||
""
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
|
||||
thisMonthClass =
|
||||
if floorMonth justViewTime == floorMonth day then
|
||||
" thismonth"
|
||||
|
||||
else
|
||||
""
|
||||
|
||||
mouseoverClass =
|
||||
setClass_ dateTimePicker.mouseOverDay " mouseover"
|
||||
|
||||
startClass =
|
||||
setClass_ dateTimePicker.startDate " start"
|
||||
|
||||
endClass =
|
||||
setClass_ dateTimePicker.endDate " end"
|
||||
|
||||
( startClassBack, endClassBack ) =
|
||||
Maybe.map2 (\sd ed -> ( startClass, endClass )) dateTimePicker.startDate dateTimePicker.endDate
|
||||
|> Maybe.withDefault ( "", "" )
|
||||
|
||||
betweenClass =
|
||||
case ( dateTimePicker.startDate, dateTimePicker.endDate ) of
|
||||
( Just start, Just end ) ->
|
||||
case ( compareDate_ start day, compareDate_ end day ) of
|
||||
( LT, GT ) ->
|
||||
" between"
|
||||
|
||||
_ ->
|
||||
""
|
||||
|
||||
_ ->
|
||||
""
|
||||
in
|
||||
div [ class ("date back" ++ startClassBack ++ endClassBack ++ betweenClass) ]
|
||||
[ div
|
||||
[ class ("date front" ++ mouseoverClass ++ startClass ++ endClass ++ thisMonthClass)
|
||||
, onMouseOver <| MouseOverDay day
|
||||
, onClick OnClickDay
|
||||
]
|
||||
[ text (Time.toDay utc day |> String.fromInt) ]
|
||||
]
|
||||
|
||||
|
||||
viewTimePicker : DateTimePicker -> StartOrEnd -> Html Msg
|
||||
viewTimePicker dateTimePicker startOrEnd =
|
||||
div
|
||||
[ class "row timepicker" ]
|
||||
[ strong [ class "subject" ]
|
||||
[ text
|
||||
(case startOrEnd of
|
||||
Start ->
|
||||
"Start"
|
||||
|
||||
End ->
|
||||
"End"
|
||||
)
|
||||
]
|
||||
, div [ class "hour" ]
|
||||
[ button
|
||||
[ class "up-button d-flex-center"
|
||||
, onClick <| IncrementTime startOrEnd InputHour 1
|
||||
]
|
||||
[ i
|
||||
[ class "fa fa-angle-up" ]
|
||||
[]
|
||||
]
|
||||
, input
|
||||
[ on "blur" <| Decode.map (SetInputTime startOrEnd InputHour) targetValueIntParse
|
||||
, value
|
||||
(case startOrEnd of
|
||||
Start ->
|
||||
case dateTimePicker.startTime of
|
||||
Just t ->
|
||||
Time.toHour utc t |> String.fromInt
|
||||
|
||||
Nothing ->
|
||||
"0"
|
||||
|
||||
End ->
|
||||
case dateTimePicker.endTime of
|
||||
Just t ->
|
||||
Time.toHour utc t |> String.fromInt
|
||||
|
||||
Nothing ->
|
||||
"0"
|
||||
)
|
||||
, maxlength 2
|
||||
, class "view d-flex-center"
|
||||
]
|
||||
[]
|
||||
, button
|
||||
[ class "down-button d-flex-center"
|
||||
, onClick <| IncrementTime startOrEnd InputHour -1
|
||||
]
|
||||
[ i
|
||||
[ class "fa fa-angle-down" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "colon d-flex-center" ] [ text ":" ]
|
||||
, div [ class "minute" ]
|
||||
[ button
|
||||
[ class "up-button d-flex-center"
|
||||
, onClick <| IncrementTime startOrEnd InputMinute 1
|
||||
]
|
||||
[ i
|
||||
[ class "fa fa-angle-up" ]
|
||||
[]
|
||||
]
|
||||
, input
|
||||
[ on "blur" <| Decode.map (SetInputTime startOrEnd InputMinute) targetValueIntParse
|
||||
, value
|
||||
(case startOrEnd of
|
||||
Start ->
|
||||
case dateTimePicker.startTime of
|
||||
Just t ->
|
||||
Time.toMinute utc t |> String.fromInt
|
||||
|
||||
Nothing ->
|
||||
"0"
|
||||
|
||||
End ->
|
||||
case dateTimePicker.endTime of
|
||||
Just t ->
|
||||
Time.toMinute utc t |> String.fromInt
|
||||
|
||||
Nothing ->
|
||||
"0"
|
||||
)
|
||||
, maxlength 2
|
||||
, class "view"
|
||||
]
|
||||
[]
|
||||
, button
|
||||
[ class "down-button d-flex-center"
|
||||
, onClick <| IncrementTime startOrEnd InputMinute -1
|
||||
]
|
||||
[ i
|
||||
[ class "fa fa-angle-down" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "timeview d-flex-center" ]
|
||||
[ text
|
||||
(let
|
||||
toString_ : Maybe Posix -> Maybe Posix -> String
|
||||
toString_ maybeTime maybeDate =
|
||||
Maybe.map
|
||||
(\t ->
|
||||
case maybeDate of
|
||||
Just d ->
|
||||
Iso8601.fromTime t
|
||||
|> String.dropRight 8
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
)
|
||||
maybeTime
|
||||
|> Maybe.withDefault ""
|
||||
|
||||
selectedTime =
|
||||
case startOrEnd of
|
||||
Start ->
|
||||
toString_ dateTimePicker.startTime dateTimePicker.startDate
|
||||
|
||||
End ->
|
||||
toString_ dateTimePicker.endTime dateTimePicker.endDate
|
||||
in
|
||||
selectedTime
|
||||
)
|
||||
]
|
||||
]
|
|
@ -21,17 +21,20 @@ view : Model -> Html Msg
|
|||
view model =
|
||||
div []
|
||||
[ renderCSS model.libUrl
|
||||
, case ( model.bootstrapCSS, model.fontAwesomeCSS ) of
|
||||
( Success _, Success _ ) ->
|
||||
, case ( model.bootstrapCSS, model.fontAwesomeCSS, model.elmDatepickerCSS ) of
|
||||
( Success _, Success _, Success _ ) ->
|
||||
div []
|
||||
[ navBar model.route
|
||||
, div [ class "container pb-4" ] [ currentView model ]
|
||||
]
|
||||
|
||||
( Failure err, _ ) ->
|
||||
( Failure err, _, _ ) ->
|
||||
failureView model err
|
||||
|
||||
( _, Failure err ) ->
|
||||
( _, Failure err, _ ) ->
|
||||
failureView model err
|
||||
|
||||
( _, _, Failure err ) ->
|
||||
failureView model err
|
||||
|
||||
_ ->
|
||||
|
@ -53,6 +56,7 @@ renderCSS assetsUrl =
|
|||
div []
|
||||
[ cssNode (assetsUrl ++ "lib/bootstrap-4.0.0-alpha.6-dist/css/bootstrap.min.css") BootstrapCSSLoaded
|
||||
, cssNode (assetsUrl ++ "lib/font-awesome-4.7.0/css/font-awesome.min.css") FontAwesomeCSSLoaded
|
||||
, cssNode (assetsUrl ++ "lib/elm-datepicker/css/elm-datepicker.css") ElmDatepickerCSSLoaded
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ module Views.SilenceForm.Types exposing
|
|||
, SilenceFormFieldMsg(..)
|
||||
, SilenceFormMsg(..)
|
||||
, emptyMatcher
|
||||
, fromDateTimePicker
|
||||
, fromMatchersAndCommentAndTime
|
||||
, fromSilence
|
||||
, initSilenceForm
|
||||
|
@ -18,9 +19,11 @@ import Data.GettableAlert exposing (GettableAlert)
|
|||
import Data.GettableSilence exposing (GettableSilence)
|
||||
import Data.Matcher exposing (Matcher)
|
||||
import Data.PostableSilence exposing (PostableSilence)
|
||||
import DateTime
|
||||
import Silences.Types exposing (nullSilence)
|
||||
import Time exposing (Posix)
|
||||
import Utils.Date exposing (addDuration, durationFormat, parseDuration, timeDifference, timeFromString, timeToString)
|
||||
import Utils.DateTimePicker.Types exposing (DateTimePicker, initDateTimePicker, initFromStartAndEndTime)
|
||||
import Utils.Filter
|
||||
import Utils.FormValidation
|
||||
exposing
|
||||
|
@ -50,6 +53,8 @@ type alias SilenceForm =
|
|||
, endsAt : ValidatedField
|
||||
, duration : ValidatedField
|
||||
, matchers : List MatcherForm
|
||||
, dateTimePicker : DateTimePicker
|
||||
, viewDateTimePicker : Bool
|
||||
}
|
||||
|
||||
|
||||
|
@ -71,6 +76,7 @@ type SilenceFormMsg
|
|||
| NewSilenceFromMatchersAndCommentAndTime String (List Utils.Filter.Matcher) String Posix
|
||||
| SilenceFetch (ApiData GettableSilence)
|
||||
| SilenceCreate (ApiData String)
|
||||
| UpdateDateTimePicker Utils.DateTimePicker.Types.Msg
|
||||
|
||||
|
||||
type SilenceFormFieldMsg
|
||||
|
@ -89,6 +95,9 @@ type SilenceFormFieldMsg
|
|||
| UpdateMatcherValue Int String
|
||||
| ValidateMatcherValue Int
|
||||
| UpdateMatcherRegex Int Bool
|
||||
| UpdateTimesFromPicker
|
||||
| OpenDateTimePicker
|
||||
| CloseDateTimePicker
|
||||
|
||||
|
||||
initSilenceForm : Key -> Model
|
||||
|
@ -124,6 +133,15 @@ toSilence { id, comment, matchers, createdBy, startsAt, endsAt } =
|
|||
|
||||
fromSilence : GettableSilence -> SilenceForm
|
||||
fromSilence { id, createdBy, comment, startsAt, endsAt, matchers } =
|
||||
let
|
||||
startsPosix =
|
||||
Utils.Date.timeFromString (DateTime.toString startsAt)
|
||||
|> Result.toMaybe
|
||||
|
||||
endsPosix =
|
||||
Utils.Date.timeFromString (DateTime.toString endsAt)
|
||||
|> Result.toMaybe
|
||||
in
|
||||
{ id = Just id
|
||||
, createdBy = initialField createdBy
|
||||
, comment = initialField comment
|
||||
|
@ -131,11 +149,13 @@ fromSilence { id, createdBy, comment, startsAt, endsAt, matchers } =
|
|||
, endsAt = initialField (timeToString endsAt)
|
||||
, duration = initialField (durationFormat (timeDifference startsAt endsAt) |> Maybe.withDefault "")
|
||||
, matchers = List.map fromMatcher matchers
|
||||
, dateTimePicker = initFromStartAndEndTime startsPosix endsPosix
|
||||
, viewDateTimePicker = False
|
||||
}
|
||||
|
||||
|
||||
validateForm : SilenceForm -> SilenceForm
|
||||
validateForm { id, createdBy, comment, startsAt, endsAt, duration, matchers } =
|
||||
validateForm { id, createdBy, comment, startsAt, endsAt, duration, matchers, dateTimePicker } =
|
||||
{ id = id
|
||||
, createdBy = validate stringNotEmpty createdBy
|
||||
, comment = validate stringNotEmpty comment
|
||||
|
@ -143,6 +163,8 @@ validateForm { id, createdBy, comment, startsAt, endsAt, duration, matchers } =
|
|||
, endsAt = validate (parseEndsAt startsAt.value) endsAt
|
||||
, duration = validate parseDuration duration
|
||||
, matchers = List.map validateMatcherForm matchers
|
||||
, dateTimePicker = dateTimePicker
|
||||
, viewDateTimePicker = False
|
||||
}
|
||||
|
||||
|
||||
|
@ -177,6 +199,8 @@ empty =
|
|||
, endsAt = initialField ""
|
||||
, duration = initialField ""
|
||||
, matchers = []
|
||||
, dateTimePicker = initDateTimePicker
|
||||
, viewDateTimePicker = False
|
||||
}
|
||||
|
||||
|
||||
|
@ -209,6 +233,8 @@ fromMatchersAndCommentAndTime defaultCreator matchers comment now =
|
|||
else
|
||||
List.filterMap (filterMatcherToMatcher >> Maybe.map fromMatcher) matchers
|
||||
, comment = initialField comment
|
||||
, dateTimePicker = initFromStartAndEndTime (Just now) (Just (addDuration defaultDuration now))
|
||||
, viewDateTimePicker = False
|
||||
}
|
||||
|
||||
|
||||
|
@ -239,3 +265,17 @@ fromMatcher { name, value, isRegex } =
|
|||
, value = initialField value
|
||||
, isRegex = isRegex
|
||||
}
|
||||
|
||||
|
||||
fromDateTimePicker : SilenceForm -> DateTimePicker -> SilenceForm
|
||||
fromDateTimePicker { id, createdBy, comment, startsAt, endsAt, duration, matchers, dateTimePicker } newPicker =
|
||||
{ id = id
|
||||
, createdBy = createdBy
|
||||
, comment = comment
|
||||
, startsAt = startsAt
|
||||
, endsAt = endsAt
|
||||
, duration = duration
|
||||
, matchers = matchers
|
||||
, dateTimePicker = newPicker
|
||||
, viewDateTimePicker = True
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ import Task
|
|||
import Time
|
||||
import Types exposing (Msg(..))
|
||||
import Utils.Date exposing (timeFromString)
|
||||
import Utils.DateTimePicker.Types exposing (initFromStartAndEndTime)
|
||||
import Utils.DateTimePicker.Updates as DateTimePickerUpdates
|
||||
import Utils.Filter exposing (silencePreviewFilter)
|
||||
import Utils.FormValidation exposing (fromResult, stringNotEmpty, updateValue, validate)
|
||||
import Utils.FormValidation exposing (fromResult, initialField, stringNotEmpty, updateValue, validate)
|
||||
import Utils.List
|
||||
import Utils.Types exposing (ApiData(..))
|
||||
import Views.SilenceForm.Types
|
||||
|
@ -18,6 +20,7 @@ import Views.SilenceForm.Types
|
|||
, SilenceFormFieldMsg(..)
|
||||
, SilenceFormMsg(..)
|
||||
, emptyMatcher
|
||||
, fromDateTimePicker
|
||||
, fromMatchersAndCommentAndTime
|
||||
, fromSilence
|
||||
, parseEndsAt
|
||||
|
@ -172,6 +175,50 @@ updateForm msg form =
|
|||
in
|
||||
{ form | matchers = matchers }
|
||||
|
||||
UpdateTimesFromPicker ->
|
||||
let
|
||||
( startsAt, endsAt, duration ) =
|
||||
case ( form.dateTimePicker.startTime, form.dateTimePicker.endTime ) of
|
||||
( Just start, Just end ) ->
|
||||
( validate timeFromString (initialField (Utils.Date.timeToString start))
|
||||
, validate (parseEndsAt (Utils.Date.timeToString start)) (initialField (Utils.Date.timeToString end))
|
||||
, initialField (Utils.Date.durationFormat (Utils.Date.timeDifference start end) |> Maybe.withDefault "")
|
||||
|> validate Utils.Date.parseDuration
|
||||
)
|
||||
|
||||
_ ->
|
||||
( form.startsAt, form.endsAt, form.duration )
|
||||
in
|
||||
{ form
|
||||
| startsAt = startsAt
|
||||
, endsAt = endsAt
|
||||
, duration = duration
|
||||
, viewDateTimePicker = False
|
||||
}
|
||||
|
||||
OpenDateTimePicker ->
|
||||
let
|
||||
startsAtTime =
|
||||
case timeFromString form.startsAt.value of
|
||||
Ok time ->
|
||||
Just time
|
||||
|
||||
_ ->
|
||||
form.dateTimePicker.startTime
|
||||
|
||||
endsAtTime =
|
||||
timeFromString form.endsAt.value |> Result.toMaybe
|
||||
in
|
||||
{ form
|
||||
| viewDateTimePicker = True
|
||||
, dateTimePicker = initFromStartAndEndTime startsAtTime endsAtTime
|
||||
}
|
||||
|
||||
CloseDateTimePicker ->
|
||||
{ form
|
||||
| viewDateTimePicker = False
|
||||
}
|
||||
|
||||
|
||||
update : SilenceFormMsg -> Model -> String -> String -> ( Model, Cmd Msg )
|
||||
update msg model basePath apiUrl =
|
||||
|
@ -269,5 +316,16 @@ update msg model basePath apiUrl =
|
|||
, Cmd.none
|
||||
)
|
||||
|
||||
UpdateDateTimePicker subMsg ->
|
||||
let
|
||||
newPicker =
|
||||
DateTimePickerUpdates.update subMsg model.form.dateTimePicker
|
||||
in
|
||||
( { model
|
||||
| form = fromDateTimePicker model.form newPicker
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
port persistDefaultCreator : String -> Cmd msg
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
module Views.SilenceForm.Views exposing (view)
|
||||
|
||||
import Data.GettableAlert exposing (GettableAlert)
|
||||
import Html exposing (Html, a, button, div, fieldset, h1, input, label, legend, span, strong, text, textarea)
|
||||
import Html.Attributes exposing (class, href)
|
||||
import Html exposing (Html, a, button, div, fieldset, h1, h5, i, input, label, legend, span, strong, text, textarea)
|
||||
import Html.Attributes exposing (class, href, style)
|
||||
import Html.Events exposing (onClick)
|
||||
import Utils.DateTimePicker.Views exposing (viewDateTimePicker)
|
||||
import Utils.Filter exposing (SilenceFormGetParams, emptySilenceFormGetParams)
|
||||
import Utils.FormValidation exposing (ValidatedField, ValidationState(..))
|
||||
import Utils.Types exposing (ApiData)
|
||||
|
@ -44,9 +45,52 @@ view maybeId { matchers, comment } defaultCreator { form, silenceId, alerts, act
|
|||
[ informationBlock activeAlertId silenceId alerts
|
||||
, silenceActionButtons maybeId form resetClick
|
||||
]
|
||||
, dateTimePickerDialog form
|
||||
]
|
||||
|
||||
|
||||
dateTimePickerDialog : SilenceForm -> Html SilenceFormMsg
|
||||
dateTimePickerDialog form =
|
||||
case form.viewDateTimePicker of
|
||||
True ->
|
||||
div []
|
||||
[ div [ class "modal fade show", style "display" "block" ]
|
||||
[ div [ class "modal-dialog modal-dialog-centered" ]
|
||||
[ div [ class "modal-content" ]
|
||||
[ div [ class "modal-header" ]
|
||||
[ button
|
||||
[ class "close ml-auto"
|
||||
, onClick (CloseDateTimePicker |> UpdateField)
|
||||
]
|
||||
[ text "x" ]
|
||||
]
|
||||
, div [ class "modal-body" ]
|
||||
[ viewDateTimePicker form.dateTimePicker |> Html.map UpdateDateTimePicker ]
|
||||
, div [ class "modal-footer" ]
|
||||
[ button
|
||||
[ class "ml-2 btn btn-outline-success mr-auto"
|
||||
, onClick (CloseDateTimePicker |> UpdateField)
|
||||
]
|
||||
[ text "Cancel" ]
|
||||
, button
|
||||
[ class "ml-2 btn btn-primary"
|
||||
, onClick (UpdateTimesFromPicker |> UpdateField)
|
||||
]
|
||||
[ text "Set Date/Time" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "modal-backdrop fade show" ] []
|
||||
]
|
||||
|
||||
False ->
|
||||
div [ style "clip" "rect(0,0,0,0)", style "position" "fixed" ]
|
||||
[ div [ class "modal fade" ] []
|
||||
, div [ class "modal-backdrop fade" ] []
|
||||
]
|
||||
|
||||
|
||||
inputSectionPadding : String
|
||||
inputSectionPadding =
|
||||
"mt-5"
|
||||
|
@ -57,7 +101,7 @@ timeInput startsAt endsAt duration =
|
|||
div [ class <| "row " ++ inputSectionPadding ]
|
||||
[ validatedField input
|
||||
"Start"
|
||||
"col-5"
|
||||
"col-4"
|
||||
(UpdateStartsAt >> UpdateField)
|
||||
(ValidateTime |> UpdateField)
|
||||
startsAt
|
||||
|
@ -69,10 +113,26 @@ timeInput startsAt endsAt duration =
|
|||
duration
|
||||
, validatedField input
|
||||
"End"
|
||||
"col-5"
|
||||
"col-4 pr-0"
|
||||
(UpdateEndsAt >> UpdateField)
|
||||
(ValidateTime |> UpdateField)
|
||||
endsAt
|
||||
, div
|
||||
[ class "flex-column form-group"
|
||||
]
|
||||
[ label
|
||||
[]
|
||||
[ text "\u{00A0}" ]
|
||||
, button
|
||||
[ class "form-control cursor-pointer"
|
||||
, onClick (OpenDateTimePicker |> UpdateField)
|
||||
]
|
||||
[ i
|
||||
[ class "fa fa-calendar"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue