Add project selection to time tracker.
[?]
Aug 19, 2020, 5:10 AM
QU5FW67RGCWOWT2YFM4NYMJFFHWIRPQANQBBAHBKZUY7UYMCSIMQCDependencies
- [2]
PT4276XCAdd logout functionality. - [3]
UOG5H2TWDefault work logging credit to logged-in user. - [4]
XJ4EYMIHLet curl prompt for http password, rather than bash. - [5]
TKGBRIQTLogin component now raises LoginComplete message. - [6]
WRPIYG3EUse project listing functionality to check for whether we have a cookie. - [7]
E7GQXOIDAllow the use of a local .env file to store username/project ID for UI scripts. - [8]
CDHZL3RPAdd a couple of other CLI utilities for interacing with the service. - [9]
NSRSSSTRUpdate nginx.conf, make aftok host configurable for cli scripts. - [10]
NJNMO72SAdd zcash.com submodule and update client to modern halogen. - [11]
TUA4HMUDUse real API capability for login. - [12]
JXG3FCXYUpgrade ps + halogen versions. - [13]
BFZN4SUAMake timeline component work. - [14]
EA5BFM5GSplit Login component into its own module. - [*]
RB2ETNIFAdd skeletal PureScript client project.
Change contents
- replacement in client/src/Aftok/Login.purs at line 78
[ P.classes (ClassName <$> ["row", "no-gutters"]) ][ P.classes (ClassName <$> ["row", "no-gutters", "container"]) ] - edit in client/src/Aftok/Project.purs at line 9
import Data.Array (index) - edit in client/src/Aftok/Project.purs at line 16
-- import Data.HTTP.Method (Method(POST)) - replacement in client/src/Aftok/Project.purs at line 17
import Data.Traversable (traverse)import Data.Traversable (traverse, traverse_) - replacement in client/src/Aftok/Project.purs at line 22
import Effect.Class (liftEffect)import Effect.Class as EC - replacement in client/src/Aftok/Project.purs at line 27
-- import Halogen as H-- import Halogen.HTML.Core (ClassName(..))-- import Halogen.HTML as HH-- import Halogen.HTML.CSS as CSS-- import Halogen.HTML.Events as E-- import Web.Event.Event as WE-- import Halogen.HTML.Properties as Pimport Aftok.Types (APIError(..))import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Events as Eimport Halogen.HTML.Properties as Pimport Effect.Class.Console (error)newtype ProjectId = ProjectId UUIDpidStr :: ProjectId -> StringpidStr (ProjectId uuid) = show uuid - replacement in client/src/Aftok/Project.purs at line 42
{ projectName :: String{ projectId :: ProjectId, projectName :: String - edit in client/src/Aftok/Project.purs at line 49
type ProjectCState ={ projects :: Array Project}data ProjectAction= Initialize| Select Inttype ProjectListSlot id = forall query. H.Slot query Project id - replacement in client/src/Aftok/Project.purs at line 64
data APIError= Forbidden| ParseFailure Json String| Error { status :: Maybe StatusCode, message :: String }projectListComponent:: forall query input m. EC.MonadEffect m=> Capability m-> H.Component HH.HTML query input Project mprojectListComponent caps = H.mkComponent{ initialState, render, eval: H.mkEval $ H.defaultEval{ handleAction = eval, initialize = Just Initialize}} whereinitialState :: input -> ProjectCStateinitialState _ = { projects: [] }render :: forall slots. ProjectCState -> H.ComponentHTML ProjectAction slots mrender st =let renderOption (Project' p) =HH.option [P.value $ pidStr p.projectId] [HH.text p.projectName]in HH.select[E.onSelectedIndexChange (Just <<< Select)]([HH.option [P.selected true, P.disabled true] [HH.text "Select a project"]] <> map renderOption st.projects)eval :: ProjectAction -> H.HalogenM ProjectCState ProjectAction () Project m Uniteval = case _ ofInitialize -> dores <- lift caps.listProjectscase res ofLeft _ -> error "Could not retrieve project list."Right projects -> H.modify_ (_ { projects = projects })Select i -> doprojects <- H.gets (_.projects)traverse_ H.raise (index projects i) - replacement in client/src/Aftok/Project.purs at line 105
projectName <- x .: "projectName"inceptionDate <- x .: "inceptionDate"initiatorStr <- x .: "initiator"initiator <- note "Failed to decode initiator UUID" $ parseUUID initiatorStrpure $ Project' { projectName, inceptionDate, initiator }project <- x .: "project"projectIdStr <- x .: "projectId"projectId <- ProjectId <$> (note "Failed to decode project UUID" $ parseUUID projectIdStr) - edit in client/src/Aftok/Project.purs at line 110
projectName <- project .: "projectName"inceptionDate <- project .: "inceptionDate"initiatorStr <- project .: "initiator"initiator <- note "Failed to decode initiator UUID" $ parseUUID initiatorStrpure $ Project' { projectId, projectName, inceptionDate, initiator } - replacement in client/src/Aftok/Project.purs at line 119
liftEffect <<< runExceptT $ case result ofEC.liftEffect <<< runExceptT $ case result of - replacement in client/src/Aftok/Timeline.purs at line 12
import Data.Maybe (Maybe(..), maybe)import Data.Either (Either(..))import Data.Maybe (Maybe(..), maybe, isJust)import Data.Symbol (SProxy(..))import Data.Time.Duration (Milliseconds(..), Days(..))import Data.Traversable (traverse_) - replacement in client/src/Aftok/Timeline.purs at line 18
import Data.Time.Duration (Milliseconds(..), Days(..))import Data.UUID as UUID - edit in client/src/Aftok/Timeline.purs at line 28
import Affjax (post, printError)import Affjax.StatusCode (StatusCode(..))import Affjax.RequestBody as RBimport Affjax.ResponseFormat as RFimport Data.Argonaut.Encode (encodeJson) - replacement in client/src/Aftok/Timeline.purs at line 50
type TimelineConfig ={ width :: Number}import Aftok.Project as Projectimport Aftok.Project (Project, Project'(..), ProjectId(..))import Aftok.Types (APIError(..))import Effect.Class.Console (log) - edit in client/src/Aftok/Timeline.purs at line 71
, selectedProject :: Maybe Project - edit in client/src/Aftok/Timeline.purs at line 76
| ProjectSelected Project.Project - edit in client/src/Aftok/Timeline.purs at line 81
data TimelineError= LogFailure (APIError) - edit in client/src/Aftok/Timeline.purs at line 86
type Slots =( projectList :: Project.ProjectListSlot Unit)_projectList = SProxy :: SProxy "projectList" - replacement in client/src/Aftok/Timeline.purs at line 93
{ logStart :: m Instant, logEnd :: m Instant{ logStart :: ProjectId -> m (Either TimelineError Instant), logEnd :: ProjectId -> m (Either TimelineError Instant) - replacement in client/src/Aftok/Timeline.purs at line 97
component :: forall query input output. Capability Aff -> TimelineConfig -> H.Component HH.HTML query input output Affcomponent caps conf = H.mkComponentcomponent:: forall query input output. Capability Aff-> Project.Capability Aff-> H.Component HH.HTML query input output Affcomponent caps pcaps = H.mkComponent - replacement in client/src/Aftok/Timeline.purs at line 112
let limits = { start: bottom, current: bottom, end: bottom }history = []active = Nothingin { limits, history, active }{ limits: { start: bottom, current: bottom, end: bottom }, history: [], active: Nothing, selectedProject: Nothing} - replacement in client/src/Aftok/Timeline.purs at line 118
render :: forall slots m. TimelineState -> H.ComponentHTML TimelineAction slots mrender :: TimelineState -> H.ComponentHTML TimelineAction Slots Aff - replacement in client/src/Aftok/Timeline.purs at line 120[3.302741]→[3.302741:303205](∅→∅),[3.303205]→[3.503:591](∅→∅),[3.591]→[3.303270:303312](∅→∅),[3.303270]→[3.303270:303312](∅→∅),[3.303312]→[3.592:728](∅→∅),[3.728]→[3.303391:303451](∅→∅),[3.303391]→[3.303391:303451](∅→∅),[3.303451]→[3.729:849](∅→∅)
HH.section[P.classes (ClassName <$> ["section-border", "border-primary"])][HH.div[P.classes (ClassName <$> ["container", "pt-6"])][HH.h1[P.classes (ClassName <$> ["mb-0", "font-weight-bold", "text-center"])][HH.text "Time Tracker"],HH.p[P.classes (ClassName <$> ["col-md-5", "text-muted", "text-center", "mx-auto"])][HH.text "Today's project timeline"],lineHtml (intervalHtml conf st.limits <$> st.history <> fromMaybe st.active),HH.div_[HH.button[P.classes (ClassName <$> ["btn", "btn-primary", "float-left"]),E.onClick \_ -> Just Start][HH.text "Start Work"],HH.button[P.classes (ClassName <$> ["btn", "btn-primary", "float-right"]),E.onClick \_ -> Just Stoplet lineForm =[lineHtml (intervalHtml st.limits <$> st.history <> fromMaybe st.active),HH.div_[HH.button[P.classes (ClassName <$> ["btn", "btn-primary", "float-left"]),E.onClick \_ -> Just Start][HH.text "Start Work"],HH.button[P.classes (ClassName <$> ["btn", "btn-primary", "float-right"]),E.onClick \_ -> Just Stop][HH.text "Stop Work"] - edit in client/src/Aftok/Timeline.purs at line 134
[HH.text "Stop Work"] - replacement in client/src/Aftok/Timeline.purs at line 135
]]in HH.section[P.classes (ClassName <$> ["section-border", "border-primary"])]([HH.div[P.classes (ClassName <$> ["container-fluid", "pt-6"])][HH.h1[P.classes (ClassName <$> ["mb-0", "font-weight-bold", "text-center"])][HH.text "Time Tracker"],HH.p[P.classes (ClassName <$> ["col-md-5", "text-muted", "text-center", "mx-auto"])][HH.text "Today's project timeline"],HH.div_[HH.slot _projectList unit (Project.projectListComponent pcaps) unit (Just <<< ProjectSelected)]]] <> (if isJust st.selectedProject then lineForm else [])) - replacement in client/src/Aftok/Timeline.purs at line 150
eval :: TimelineAction -> H.HalogenM TimelineState TimelineAction () output Aff Uniteval :: TimelineAction -> H.HalogenM TimelineState TimelineAction Slots output Aff Unit - edit in client/src/Aftok/Timeline.purs at line 167
, selectedProject: Nothing - edit in client/src/Aftok/Timeline.purs at line 172
ProjectSelected p ->H.modify_ (_ { selectedProject = Just p }) - replacement in client/src/Aftok/Timeline.purs at line 176
t <- lift caps.logStartH.modify_ (start t)let withProject (Project' p) = dologged <- lift $ caps.logStart p.projectIdcase logged ofLeft _ -> log "Failed to start timer."Right t -> H.modify_ (start t)project <- H.gets (_.selectedProject)log $ "Project selected? " <> show (isJust project)traverse_ withProject project - replacement in client/src/Aftok/Timeline.purs at line 186
t <- lift caps.logEndH.modify_ (stop t)let withProject (Project' p) = dologged <- lift $ caps.logEnd p.projectIdcase logged ofLeft _ -> log "Failed to stop timer."Right t -> H.modify_ (stop t)project <- H.gets (_.selectedProject)traverse_ withProject project - replacement in client/src/Aftok/Timeline.purs at line 215
. TimelineConfig-> TimelineLimits. TimelineLimits - replacement in client/src/Aftok/Timeline.purs at line 218
intervalHtml conf limits i =intervalHtml limits i = - edit in client/src/Aftok/Timeline.purs at line 267
logStart :: ProjectId -> Aff (Either TimelineError Instant)logStart (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }result <- post RF.json ("/api/projects/" <> UUID.toString pid <> "/logStart") requestBodycase result ofLeft err -> pure <<< Left <<< LogFailure $ Error { status: Nothing, message: printError err }Right r -> case r.status ofStatusCode 403 -> pure <<< Left <<< LogFailure $ ForbiddenStatusCode 200 -> Right <$> liftEffect nowother -> pure <<< Left <<< LogFailure $ Error { status: Just other, message: r.statusText }logEnd :: ProjectId -> Aff (Either TimelineError Instant)logEnd (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }result <- post RF.json ("/api/projects/" <> UUID.toString pid <> "/logEnd") requestBodycase result ofLeft err -> pure <<< Left <<< LogFailure $ Error { status: Nothing, message: printError err }Right r -> case r.status ofStatusCode 403 -> pure <<< Left <<< LogFailure $ ForbiddenStatusCode 200 -> Right <$> liftEffect nowother -> pure <<< Left <<< LogFailure $ Error { status: Just other, message: r.statusText }apiCapability :: Capability AffapiCapability = { logStart, logEnd } - replacement in client/src/Aftok/Timeline.purs at line 294
{ logStart: liftEffect now, logEnd: liftEffect now{ logStart: \_ -> Right <$> liftEffect now, logEnd: \_ -> Right <$> liftEffect now - file addition: Types.purs[3.1]
module Aftok.Types whereimport Data.Argonaut.Core (Json)import Data.Maybe (Maybe)import Affjax.StatusCode (StatusCode)data APIError= Forbidden| ParseFailure Json String| Error { status :: Maybe StatusCode, message :: String } - replacement in client/src/Main.purs at line 32
timeline = Timeline.mockCapabilitytimeline = Timeline.apiCapability - replacement in client/src/Main.purs at line 83
[ HH.slot _timeline unit (Timeline.component tlCap { width: 600.0 }) unit absurd ][ HH.slot _timeline unit (Timeline.component tlCap pCap) unit absurd ] - replacement in scripts/list_projects.sh at line 16
curl --verbose --insecure --user $USER \curl --insecure --user $USER \