mywork-1.0.1.0: lib/Panes/AddProj.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Panes.AddProj
(
AddProjPane
, initAddProj
, isAddProjActive
, projectInputResults
)
where
import Brick hiding ( Location )
import Brick.Focus
import Brick.Forms
import Brick.Panes
import qualified Brick.Widgets.Center as C
import qualified Brick.Widgets.Core as BC
import qualified Brick.Widgets.Table as BT
import Control.Lens hiding ( under )
import Data.Either ( isRight )
import qualified Data.List as DL
import Data.Maybe ( isJust )
import qualified Data.Sequence as Seq
import Data.Text ( Text )
import qualified Data.Text as T
import Data.Time.Calendar ( Day )
import qualified Graphics.Vty as Vty
import Defs
import Panes.Common.Attrs
import Panes.Common.Inputs
import Sync
data AddProjPane
data NewProj = NewProj { _npName :: ProjectName
, _npRole :: Role
, _npGroupG :: Maybe Group
, _npGroupT :: Text
, _npLangR :: Either Text Language
, _npLangT :: Text
, _npDesc :: Text
, _npLoc :: LocationSpec
, _npLocDate :: Maybe Day
}
makeLenses ''NewProj
blankNewProj :: NewProj
blankNewProj = NewProj (ProjectName "") User (Just Personal) "" (Right C)
"" "" (RemoteSpec "") Nothing
type ProjForm = Form NewProj MyWorkEvent WName
instance Pane WName MyWorkEvent AddProjPane where
data (PaneState AddProjPane MyWorkEvent) = NP { nPF :: Maybe ProjForm
-- Just == pane active
, nPrj :: Maybe Project
-- reset to Nothing when nPF
-- transitions Nothing -> Just
, nOrig :: Maybe Project
, nErr :: Maybe Text
}
type (EventType AddProjPane WName MyWorkEvent) = BrickEvent WName MyWorkEvent
initPaneState _ = NP Nothing Nothing Nothing Nothing
drawPane ps _gs =
C.centerLayer
. modalB ((maybe "New" (const "Edit") $ nOrig ps) <> " Project")
. vLimitPercent 80
. hLimitPercent 65
. (\f -> vBox [ renderForm f
, padBottom (Pad 1) $ withAttr a'Error
$ maybe (txt " ") txt (nErr ps)
, emptyWidget
, vLimit 1 (fill ' ' <+> str "Ctrl-D = accept"
<+> fill ' ' <+> str "ESC = abort"
<+> fill ' ')
]) <$> nPF ps
focusable _ ps = case nPF ps of
Nothing -> mempty
Just f -> Seq.fromList $ focusRingToList $ formFocus f
handlePaneEvent _ = \case
VtyEvent (Vty.EvKey Vty.KEsc []) -> nPFL %%~ const (return Nothing)
VtyEvent (Vty.EvKey (Vty.KChar 'd') [Vty.MCtrl]) -> \s ->
let pf = s ^. nPFL
np form = Project { projName = form ^. npName
, group = case form ^. npGroupG of
Just r -> r
Nothing -> OtherGroup $ form ^. npGroupT
, role = form ^. npRole
, language = case form ^. npLangR of
r@(Right _) -> r
Left _ -> Left $ form ^. npLangT
, description = form ^. npDesc
, locations =
case form ^. npLoc of
RemoteSpec rs | T.null rs -> mempty
_ -> [ Location
{ location = form ^. npLoc
, locatedOn = form ^. npLocDate
, notes = mempty
, locCore = LocRT
{ locValid = True -- assumed
}
}
]
, projCore = ProjRT
}
in if maybe False allFieldsValid pf
then do let p0 = np . formState <$> pf
p <- case p0 of
Nothing -> return Nothing
Just jp -> Just <$> syncProject jp
return $ s & nPFL .~ Nothing & newProject .~ p
else
let badflds = maybe "none"
(foldr (\n a -> if T.null a
then T.pack n
else T.pack n <> ", " <> a) ""
. fmap show . invalidFields)
pf
errmsg = "Correct invalid entries before accepting: "
in return $ s { nErr = Just $ errmsg <> badflds }
ev -> \s -> validateForm
$ s { nErr = Nothing }
& (nPFL . _Just %%~ \w -> nestEventM' w (handleFormEvent ev))
nPFL :: Lens' (PaneState AddProjPane MyWorkEvent) (Maybe ProjForm)
nPFL f s = (\n -> s { nPF = n }) <$> f (nPF s)
isAddProjActive :: PaneState AddProjPane MyWorkEvent -> Bool
isAddProjActive = isJust . nPF
newProject :: Lens' (PaneState AddProjPane MyWorkEvent) (Maybe Project)
newProject f s = (\n -> s { nPrj = n}) <$> f (nPrj s)
-- | Returns the original project name (if any) and the new Project
-- specification.
projectInputResults :: PaneState AddProjPane MyWorkEvent
-> (Maybe ProjectName, Maybe Project)
projectInputResults ps = (view projNameL <$> nOrig ps, nPrj ps)
validateForm :: EventM WName es (PaneState AddProjPane MyWorkEvent)
-> EventM WName es (PaneState AddProjPane MyWorkEvent)
validateForm inner = do
s <- inner
case s ^. nPFL of
Nothing -> return s
Just pf -> do
let isOK1 = or [ formState pf ^. npGroupG /= Nothing
, formState pf ^. npGroupT /= ""
]
let tgtfld1 = WName "Other Group Text"
let isOK2 = or [ isRight (formState pf ^. npLangR)
, formState pf ^. npLangT /= ""
]
let tgtfld2 = WName "Other Language Name"
(ltgt, lvalid) <- validateLocationInput True $ formState pf ^. npLoc
return $ s
& nPFL %~ fmap (setFieldValid isOK1 tgtfld1)
& nPFL %~ fmap (setFieldValid isOK2 tgtfld2)
& nPFL %~ fmap (setFieldValid lvalid ltgt)
initAddProj :: Projects
-> Maybe Project
-> PaneState AddProjPane MyWorkEvent
-> PaneState AddProjPane MyWorkEvent
initAddProj prjs mbProj ps =
case nPF ps of
Just _ -> ps
Nothing ->
let label s = padBottom (Pad 1) . label' s
label' s w = (vLimit 1 $ hLimit labelWidth
$ fill ' ' <+> str s <+> str ": ") <+> w
under s w = padBottom (Pad 1)
$ vLimit 1
$ padLeft (Pad (labelWidth + 4))
$ str s <+> w
labelWidth = 18
numCols lastSolo nc =
let go wdgs =
if null wdgs then []
else fmap padded (DL.take nc (wdgs <> DL.repeat emptyWidget))
: go (DL.drop nc wdgs)
padded = padRight (BC.Pad 2)
renderT = BT.renderTable
. BT.surroundingBorder False
. BT.rowBorders False
. BT.columnBorders False
in if lastSolo
then \wdgs ->
if null wdgs then emptyWidget
else (renderT $ BT.table $ go $ DL.init wdgs)
<=> DL.last wdgs
else renderT . BT.table . go
projFields =
[ label "Project name" @@=
let validate = \case
[] -> Nothing
[""] -> Nothing
(nmt:_) ->
let nm = ProjectName nmt
in if nm `elem` (view projNameL <$> projects prjs)
&& (maybe True ((nm /=) . view projNameL) mbProj)
then Nothing -- invalid
else Just nm
in editField npName (WName "New Project Name") (Just 1)
(\(ProjectName nm) -> nm) validate (txt . headText) id
, label' "Group" @@=
radioField npGroupG
[ (Just Personal, (WName "+Prj:Grp:Personal"), "Personal")
, (Just Work, (WName "+Prj:Grp:Work"), "Work")
, (Nothing, (WName "Other Group Text"), "Other")
]
, under "...: " @@=
editTextField npGroupT (WName "+Proj:Grp:Text") (Just 1)
, label "Role" @@=
-- setFieldConcat (hBox . DL.intersperse (str " ")) .
setFieldConcat (numCols False 2)
. radioField npRole
[ (Author, (WName "+Prj:Role:Author"), "Author")
, (Maintainer, (WName "+Prj:Role:Maintainer"), "Maintainer")
, (Contributor, (WName "+Prj:Role:Contributor"), "Contributor")
, (User, (WName "+Prj:Role:User"), "User")
]
, label' "Language" @@=
setFieldConcat (numCols True 4)
. radioField npLangR
[ (Right C, (WName "+Prj:Lang:C"), "C")
, (Right CPlusPlus, (WName "+Prj:Lang:CPP"), "C++")
, (Right Haskell, (WName "+Prj:Lang:Haskell"), "Haskell")
, (Right JavaScript, (WName "+Prj:Lang:JS"), "JavaScript")
, (Right Prolog, (WName "+Prj:Lang:Prolog"), "Prolog")
, (Right Python, (WName "+Prj:Lang:Python"), "Python")
, (Right Rust, (WName "+Prj:Lang:Rust"), "Rust")
, (Left "", (WName "Other Language Name"), "Custom")
]
, under "...: " @@=
editTextField npLangT (WName "+Prj:Lang:Text") (Just 1)
, label "Description" @@=
editTextField npDesc (WName "+Prj:Desc") Nothing
]
locFields =
case mbProj of
Nothing ->
-- Only query for the initial location if this is a new project;
-- do not query for an existing project.
[
label "Initial location" @@=
locationInput mempty Nothing True npLoc
, label "Location date" @@= mbDateInput npLocDate
]
_ -> []
npForm =
newForm (projFields <> locFields)
(case mbProj of
Nothing -> blankNewProj
Just p -> NewProj { _npName = p ^. projNameL
, _npRole = p ^. roleL
, _npGroupG = case p ^. groupL of
Personal -> Just Personal
Work -> Just Work
OtherGroup _ -> Nothing
, _npGroupT = case p ^. groupL of
OtherGroup t -> t
_ -> ""
, _npLangR = p ^. languageL
, _npLangT = case p ^. languageL of
Right _ -> ""
Left t -> t
, _npDesc = p ^. descriptionL
, _npLoc = RemoteSpec ""
, _npLocDate = Nothing
}
)
in NP { nPF = Just npForm
, nPrj = Nothing
, nOrig = mbProj
, nErr = Nothing
}