display API/Validation errors in UI

This commit is contained in:
Jon Schoning 2021-10-02 22:31:51 -05:00 committed by Yann Esposito (Yogsototh)
parent 9682a0c9c1
commit 5f178e59bd
Signed by untrusted user who does not match committer: yogsototh
GPG key ID: 7B19A4C650D59646
12 changed files with 124 additions and 60 deletions

View file

@ -48,9 +48,9 @@ editBookmark :: Bookmark -> Aff (Either Error (Response String))
editBookmark bm = do
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.string
editNote :: Note -> Aff (Either Error (Response Json))
editNote :: Note -> Aff (Either Error (Response String))
editNote bm = do
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.json
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.string
lookupTitle :: Bookmark -> Aff (Maybe String)
lookupTitle bm = do

View file

@ -10,7 +10,7 @@ import Halogen as H
import Halogen.HTML (div, input, text)
import Halogen.HTML.Elements (label)
import Halogen.HTML.Events (onChecked)
import Halogen.HTML.Properties (InputType(..), checked, for, id_, name, type_)
import Halogen.HTML.Properties (InputType(..), checked, for, id, name, type_)
import Model (AccountSettings)
import Util (class_)
import Web.Event.Event (Event)
@ -52,19 +52,19 @@ usetting u' =
div [ class_ "settings-form" ]
[ div [ class_ "fw7 mb2"] [ text "Account Settings" ]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "archiveDefault", name "archiveDefault"
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "archiveDefault", name "archiveDefault"
, checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ]
, label [ for "archiveDefault", class_ "lh-copy" ]
[ text "Archive Non-Private Bookmarks (archive.li)" ]
]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privateDefault", name "privateDefault"
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privateDefault", name "privateDefault"
, checked (us.privateDefault) , onChecked (editField EprivateDefault) ]
, label [ for "privateDefault", class_ "lh-copy" ]
[ text "Default new bookmarks to Private" ]
]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privacyLock", name "privacyLock"
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privacyLock", name "privacyLock"
, checked (us.privacyLock) , onChecked (editField EprivacyLock) ]
, label [ for "privacyLock", class_ "lh-copy" ]
[ text "Privacy Lock (Private Account)" ]

View file

@ -7,18 +7,18 @@ import Affjax.StatusCode (StatusCode(..))
import App (destroy, editBookmark, lookupTitle)
import Data.Either (Either(..))
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), maybe, isJust)
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
import Data.Monoid (guard)
import Data.String (Pattern(..), null, stripPrefix)
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Globals (app', closeWindow, mmoment8601)
import Globals (closeWindow, mmoment8601)
import Halogen as H
import Halogen.HTML (button, div, form, input, label, p, span, table, tbody_, td, td_, text, textarea, tr_)
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, autofocus, checked, disabled, for, id_, name, required, rows, title, type_, value)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, autofocus, checked, disabled, for, id, name, required, rows, title, type_, value)
import Model (Bookmark)
import Util (_curQuerystring, _loc, _doc, _lookupQueryStringValue, attr, class_, ifElseH, whenH)
import Web.Event.Event (Event, preventDefault)
@ -47,6 +47,7 @@ type BState =
, deleteAsk :: Boolean
, loading :: Boolean
, destroyed :: Boolean
, apiError :: Maybe String
}
_bm :: Lens' BState Bookmark
@ -55,6 +56,9 @@ _bm = lens _.bm (_ { bm = _ })
_edit_bm :: Lens' BState Bookmark
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
_apiError :: Lens' BState (Maybe String)
_apiError = lens _.apiError (_ { apiError = _ })
addbmark :: forall q i o. Bookmark -> H.Component q i o Aff
addbmark b' =
H.mkComponent
@ -63,7 +67,6 @@ addbmark b' =
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
app = app' unit
mkState b =
{ bm: b
@ -71,10 +74,11 @@ addbmark b' =
, deleteAsk: false
, destroyed: false
, loading: false
, apiError: Nothing
}
render :: forall m. BState -> H.ComponentHTML BAction () m
render s@{ bm, edit_bm } =
render s@{ bm, edit_bm, apiError } =
ifElseH (not s.destroyed)
display_edit
display_destroyed
@ -86,39 +90,41 @@ addbmark b' =
[ tr_
[ td [ class_ "w1" ] [ ]
, td_ [ whenH (bm.bid > 0)
display_exists
display_exists,
whenH (isJust apiError)
(alert_notification (fromMaybe "" apiError))
]
]
, tr_
[ td_ [ label [ for "url" ] [ text "URL" ] ]
, td_ [ input [ type_ InputUrl , id_ "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
, td_ [ input [ type_ InputUrl , id "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
, value (edit_bm.url) , onValueChange (editField Eurl)] ]
]
, tr_
[ td_ [ label [ for "title" ] [ text "title" ] ]
, td [class_ "flex"]
[ input [ type_ InputText , id_ "title", class_ "w-100 mv1 flex-auto" , name "title" , value (edit_bm.title) , onValueChange (editField Etitle)]
[ input [ type_ InputText , id "title", class_ "w-100 mv1 flex-auto" , name "title" , value (edit_bm.title) , onValueChange (editField Etitle)]
, button [ disabled s.loading, type_ ButtonButton, onClick \_ -> BLookupTitle, class_ ("ml2 input-reset ba b--navy pointer f6 di dim pa1 ma1 mr0 " <> guard s.loading "bg-light-silver") ] [ text "fetch" ]
]
]
, tr_
[ td_ [ label [ for "description" ] [ text "description" ] ]
, td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id_ "description", name "description", rows 4
, td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id "description", name "description", rows 4
, value (edit_bm.description) , onValueChange (editField Edescription)] ]
]
, tr_
[ td_ [ label [ for "tags" ] [ text "tags" ] ]
, td_ [ input [ type_ InputText , id_ "tags", class_ "w-100 mv1" , name "tags", autocomplete false, attr "autocapitalize" "off", autofocus (not $ null bm.url)
, td_ [ input [ type_ InputText , id "tags", class_ "w-100 mv1" , name "tags", autocomplete false, attr "autocapitalize" "off", autofocus (not $ null bm.url)
, value (edit_bm.tags) , onValueChange (editField Etags)] ]
]
, tr_
[ td_ [ label [ for "private" ] [ text "private" ] ]
, td_ [ input [ type_ InputCheckbox , id_ "private", class_ "private pointer" , name "private"
, td_ [ input [ type_ InputCheckbox , id "private", class_ "private pointer" , name "private"
, checked (edit_bm.private) , onChecked (editField Eprivate)] ]
]
, tr_
[ td_ [ label [ for "toread" ] [ text "read later" ] ]
, td_ [ input [ type_ InputCheckbox , id_ "toread", class_ "toread pointer" , name "toread"
, td_ [ input [ type_ InputCheckbox , id "toread", class_ "toread pointer" , name "toread"
, checked (edit_bm.toread) , onChecked (editField Etoread)] ]
]
, tr_
@ -146,6 +152,9 @@ addbmark b' =
]
]
alert_notification alert_text _ =
div [ class_ "alert alert-err" ] [ text alert_text ]
display_destroyed _ = p [ class_ "red"] [text "you killed this bookmark"]
editField :: forall a. (a -> EditField) -> a -> BAction
@ -186,8 +195,10 @@ addbmark b' =
handleAction (BEditSubmit e) = do
liftEffect (preventDefault e)
edit_bm <- use _edit_bm
_apiError .= Nothing
H.liftAff (editBookmark edit_bm) >>= case _ of
Left affErr -> do
_apiError .= Just (printError affErr)
liftEffect $ log (printError affErr)
Right { status: StatusCode s } | s >= 200 && s < 300 -> do
_bm .= edit_bm
@ -204,4 +215,5 @@ addbmark b' =
Nothing -> setHref org loc
_ -> liftEffect $ closeWindow =<< window
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -2,24 +2,29 @@ module Component.BMark where
import Prelude hiding (div)
import Affjax (printError)
import Affjax.StatusCode (StatusCode(..))
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar, lookupTitle)
import Component.Markdown as Markdown
import Data.Const (Const)
import Data.Either (Either(..))
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), fromMaybe, isJust)
import Data.Monoid (guard)
import Data.Nullable (toMaybe)
import Data.String (null, split, take, replaceAll) as S
import Data.String.Pattern (Pattern(..), Replacement(..))
import Type.Proxy (Proxy(..))
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Class.Console (log)
import Globals (app', setFocus, toLocaleDateString)
import Halogen as H
import Halogen.HTML (a, br_, button, div, div_, form, input, label, span, text, textarea)
import Halogen.HTML as HH
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, disabled, for, href, id_, name, required, rows, target, title, type_, value)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, disabled, for, href, id, name, required, rows, target, title, type_, value)
import Model (Bookmark)
import Type.Proxy (Proxy(..))
import Util (attr, class_, fromNullableStr, ifElseH, whenH, whenA)
import Web.Event.Event (Event, preventDefault)
@ -55,6 +60,7 @@ type BState =
, deleteAsk:: Boolean
, edit :: Boolean
, loading :: Boolean
, apiError :: Maybe String
}
_bm :: Lens' BState Bookmark
@ -66,6 +72,9 @@ _edit_bm = lens _.edit_bm (_ { edit_bm = _ })
_edit :: Lens' BState Boolean
_edit = lens _.edit (_ { edit = _ })
_apiError :: Lens' BState (Maybe String)
_apiError = lens _.apiError (_ { apiError = _ })
_markdown = Proxy :: Proxy "markdown"
type ChildSlots =
@ -88,11 +97,12 @@ bmark b' =
, deleteAsk: false
, edit: false
, loading: false
, apiError: Nothing
}
render :: BState -> H.ComponentHTML BAction ChildSlots Aff
render s@{ bm, edit_bm } =
div [ id_ (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
render s@{ bm, edit_bm, apiError } =
div [ id (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
[ whenH app.dat.isowner
star
, ifElseH s.edit
@ -151,7 +161,9 @@ bmark b' =
display_edit _ =
div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $
[ form [ onSubmit BEditSubmit ]
[ whenH (isJust apiError)
(alert_notification (fromMaybe "" apiError))
, form [ onSubmit BEditSubmit ]
[ div_ [ text "url" ]
, input [ type_ InputUrl , class_ "url w-100 mb2 pt1 edit_form_input" , required true , name "url"
, value (edit_bm.url) , onValueChange (editField Eurl) ]
@ -164,19 +176,19 @@ bmark b' =
, div_ [ text "description" ]
, textarea [ class_ "description w-100 mb1 pt1 edit_form_input" , name "description", rows 5
, value (edit_bm.description) , onValueChange (editField Edescription) ]
, div [ id_ "tags_input_box"]
, div [ id "tags_input_box"]
[ div_ [ text "tags" ]
, input [ id_ (tagid edit_bm), type_ InputText , class_ "tags w-100 mb1 pt1 edit_form_input" , name "tags"
, input [ id (tagid edit_bm), type_ InputText , class_ "tags w-100 mb1 pt1 edit_form_input" , name "tags"
, autocomplete false, attr "autocapitalize" "off"
, value (edit_bm.tags) , onValueChange (editField Etags) ]
]
, div [ class_ "edit_form_checkboxes mv3"]
[ input [ type_ InputCheckbox , class_ "private pointer" , id_ "edit_private", name "private"
[ input [ type_ InputCheckbox , class_ "private pointer" , id "edit_private", name "private"
, checked (edit_bm.private) , onChecked (editField Eprivate) ]
, text " "
, label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
, text " "
, input [ type_ InputCheckbox , class_ "toread pointer" , id_ "edit_toread", name "toread"
, input [ type_ InputCheckbox , class_ "toread pointer" , id "edit_toread", name "toread"
, checked (edit_bm.toread) , onChecked (editField Etoread) ]
, text " "
, label [ for "edit_toread" ] [ text "to-read" ]
@ -188,6 +200,8 @@ bmark b' =
]
]
alert_notification alert_text _ =
div [ class_ "alert alert-err" ] [ text alert_text ]
editField :: forall a. (a -> EditField) -> a -> BAction
editField f = BEditField <<< f
@ -228,6 +242,7 @@ bmark b' =
bm <- use _bm
_edit_bm .= bm
_edit .= e
_apiError .= Nothing
H.liftEffect $
when e
(setFocus (tagid bm))
@ -256,7 +271,15 @@ bmark b' =
handleAction (BEditSubmit e) = do
H.liftEffect (preventDefault e)
edit_bm <- use _edit_bm
_apiError .= Nothing
let edit_bm' = edit_bm { tags = S.replaceAll (Pattern ",") (Replacement " ") edit_bm.tags }
void $ H.liftAff (editBookmark edit_bm')
_bm .= edit_bm'
_edit .= false
H.liftAff (editBookmark edit_bm') >>= case _ of
Left affErr -> do
_apiError .= Just (printError affErr)
liftEffect $ log (printError affErr)
Right { status: StatusCode s } | s >= 200 && s < 300 -> do
_bm .= edit_bm'
_edit .= false
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -12,7 +12,7 @@ import Globals (app', mmoment8601)
import Halogen as H
import Halogen.HTML (a, br_, div, text)
import Halogen.HTML as HH
import Halogen.HTML.Properties (href, id_, title)
import Halogen.HTML.Properties (href, id, title)
import Model (Note, NoteSlug)
import Util (class_, fromNullableStr)
@ -49,7 +49,7 @@ nlist st' =
HH.div_ (map renderNote notes)
where
renderNote note =
div [ id_ (show note.id)
div [ id (show note.id)
, class_ ("note w-100 mw7 pa1 mb2"
<> if note.shared then "" else " private")] $
[ div [ class_ "display" ] $

View file

@ -2,12 +2,15 @@ module Component.NNote where
import Prelude hiding (div)
import Affjax (printError)
import Affjax.StatusCode (StatusCode(..))
import App (destroyNote, editNote)
import Component.Markdown as Markdown
import Data.Array (drop, foldMap)
import Data.Either (Either(..))
import Data.Foldable (for_)
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), isJust, maybe)
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
import Data.Monoid (guard)
import Data.String (null, split) as S
import Data.String (null, stripPrefix)
@ -15,12 +18,13 @@ import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Globals (app', mmoment8601, setFocus, closeWindow)
import Halogen as H
import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea)
import Halogen.HTML as HH
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autofocus, checked, for, id_, name, rows, title, type_, value)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autofocus, checked, for, id, name, rows, title, type_, value)
import Model (Note)
import Type.Proxy (Proxy(..))
import Util (_curQuerystring, _doc, _loc, _lookupQueryStringValue, class_, fromNullableStr, ifElseH, whenH)
@ -43,6 +47,7 @@ type NState =
, deleteAsk :: Boolean
, edit :: Boolean
, destroyed :: Boolean
, apiError :: Maybe String
}
_note :: Lens' NState Note
@ -54,6 +59,9 @@ _edit_note = lens _.edit_note (_ { edit_note = _ })
_edit :: Lens' NState Boolean
_edit = lens _.edit (_ { edit = _ })
_apiError :: Lens' NState (Maybe String)
_apiError = lens _.apiError (_ { apiError = _ })
-- | FormField Edits
data EditField
= Etitle String
@ -83,10 +91,11 @@ nnote st' =
, deleteAsk: false
, edit: note'.id <= 0
, destroyed: false
, apiError: Nothing
}
render :: NState -> H.ComponentHTML NAction ChildSlots Aff
render st@{ note, edit_note } =
render st@{ note, edit_note, apiError } =
ifElseH st.destroyed
display_destroyed
(const (ifElseH st.edit
@ -95,7 +104,7 @@ nnote st' =
where
renderNote _ =
div [ id_ (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $
div [ id (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $
[ div [ class_ "display" ] $
[ div [ class_ ("link f5 lh-title")]
[ text $ if S.null note.title then "[no title]" else note.title ]
@ -127,24 +136,26 @@ nnote st' =
renderNote_edit _ =
form [ onSubmit NEditSubmit ]
[ p [ class_ "mt2 mb1"] [ text "title:" ]
[ whenH (isJust apiError)
(alert_notification (fromMaybe "" apiError))
, p [ class_ "mt2 mb1"] [ text "title:" ]
, input [ type_ InputText , class_ "title w-100 mb1 pt1 edit_form_input" , name "title"
, value (edit_note.title) , onValueChange (editField Etitle), autofocus (null edit_note.title)
]
, br_
, p [ class_ "mt2 mb1"] [ text "description:" ]
, textarea [ id_ (notetextid edit_note), class_ "description w-100 mb1 pt1 edit_form_input" , name "text", rows 25
, textarea [ id (notetextid edit_note), class_ "description w-100 mb1 pt1 edit_form_input" , name "text", rows 25
, value (edit_note.text) , onValueChange (editField Etext)
]
, div [ class_ "edit_form_checkboxes mb3"]
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_ismarkdown", name "ismarkdown"
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id "edit_ismarkdown", name "ismarkdown"
, checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ]
, text " "
, label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ]
, br_
]
, div [ class_ "edit_form_checkboxes mb3"]
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_shared", name "shared"
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id "edit_shared", name "shared"
, checked (edit_note.shared) , onChecked (editField Eshared) ]
, text " "
, label [ for "edit_shared" , class_ "mr2" ] [ text "public?" ]
@ -163,6 +174,9 @@ nnote st' =
display_destroyed _ = p [ class_ "red"] [text "you killed this note"]
alert_notification alert_text _ =
div [ class_ "alert alert-err" ] [ text alert_text ]
mmoment n = mmoment8601 n.created
editField :: forall a. (a -> EditField) -> a -> NAction
editField f = NEditField <<< f
@ -209,21 +223,28 @@ nnote st' =
handleAction (NEditSubmit e) = do
H.liftEffect (preventDefault e)
edit_note <- use _edit_note
res' <- H.liftAff (editNote edit_note)
for_ res' \_ -> do
qs <- liftEffect _curQuerystring
doc <- liftEffect $ _doc
ref <- liftEffect $ referrer doc
loc <- liftEffect $ _loc
org <- liftEffect $ origin loc
case _lookupQueryStringValue qs "next" of
Just "closeWindow" -> liftEffect $ closeWindow =<< window
Just "back" -> liftEffect $
if isJust (stripPrefix (Pattern org) ref)
then setHref ref loc
else setHref org loc
_ -> if (edit_note.id == 0)
then liftEffect $ setHref (fromNullableStr app.noteR) =<< _loc
else do
_note .= edit_note
_edit .= false
_apiError .= Nothing
H.liftAff (editNote edit_note) >>= case _ of
Left affErr -> do
_apiError .= Just (printError affErr)
liftEffect $ log (printError affErr)
Right { status: StatusCode s } | s >= 200 && s < 300 -> do
qs <- liftEffect _curQuerystring
doc <- liftEffect $ _doc
ref <- liftEffect $ referrer doc
loc <- liftEffect $ _loc
org <- liftEffect $ origin loc
case _lookupQueryStringValue qs "next" of
Just "closeWindow" -> liftEffect $ closeWindow =<< window
Just "back" -> liftEffect $
if isJust (stripPrefix (Pattern org) ref)
then setHref ref loc
else setHref org loc
_ -> if (edit_note.id == 0)
then liftEffect $ setHref (fromNullableStr app.noteR) =<< _loc
else do
_note .= edit_note
_edit .= false
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -151,6 +151,11 @@ label {
.alert {
background: #ced;
border: 1px solid #acc;
margin-bottom: 5px;
padding: 2px;
}
.alert.alert-err {
background-color: #ffdfdf
}
.edit_bookmark_form {

View file

@ -25,6 +25,9 @@ textarea {
#addForm .alert {
margin-top: -6px;
}
.alert.alert-err {
background-color: #ffdfdf
}
form label {
margin: 0;
vertical-align: middle;

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.