Very basic support for ticket label colors

- WorkflowField now has a color, it's a simple `Maybe Int` for now. Valid
  values are only 1-4
- That color is used for displaying ticket class params a.k.a labels in ticket
  list view
- Ticket list now also serves a paged OrderedCollection

I tried to use a single SQL query to grab the tickets along with their labels,
but couldn't figure out a way to aggregate tuples/rows into an array (it seems
only single values are supported in Esqueleto). Instead of doing manual SQL or
adding Esqueleto functions, I just switched from 1 query to O(n) queries: Each
ticket has its own query selecting its labels. I guess it's slower, but also,
ticket list is paged now with fixed page size so it's really O(1) ^_^
This commit is contained in:
fr33domlover 2020-01-16 10:29:47 +00:00
parent 031a4c0930
commit f4a5866dec
11 changed files with 188 additions and 80 deletions

View file

@ -303,6 +303,7 @@ WorkflowField
filterNew Bool filterNew Bool
filterTodo Bool filterTodo Bool
filterClosed Bool filterClosed Bool
color Int Maybe
UniqueWorkflowField workflow ident UniqueWorkflowField workflow ident

View file

@ -65,6 +65,7 @@ data NewField = NewField
, nfNew :: Bool , nfNew :: Bool
, nfTodo :: Bool , nfTodo :: Bool
, nfClosed :: Bool , nfClosed :: Bool
, nfColor :: Maybe Int
} }
newFieldAForm :: WorkflowId -> AForm Handler NewField newFieldAForm :: WorkflowId -> AForm Handler NewField
@ -79,12 +80,15 @@ newFieldAForm wid = NewField
<*> areq checkBoxField "Applies to New*" (Just True) <*> areq checkBoxField "Applies to New*" (Just True)
<*> areq checkBoxField "Applies to Todo*" (Just True) <*> areq checkBoxField "Applies to Todo*" (Just True)
<*> areq checkBoxField "Applies to Closed*" (Just True) <*> areq checkBoxField "Applies to Closed*" (Just True)
<*> aopt (selectField selectColor) "Color" Nothing
where where
selectEnum = selectEnum =
optionsPersistKey optionsPersistKey
[WorkflowEnumWorkflow ==. wid] [WorkflowEnumWorkflow ==. wid]
[Asc WorkflowEnumName] [Asc WorkflowEnumName]
workflowEnumName workflowEnumName
selectColor =
optionsPairs [("red" :: Text, 1), ("green", 2), ("yellow", 3), ("blue", 4)]
newFieldForm :: WorkflowId -> Form NewField newFieldForm :: WorkflowId -> Form NewField
newFieldForm wid = renderDivs $ newFieldAForm wid newFieldForm wid = renderDivs $ newFieldAForm wid

View file

@ -59,6 +59,7 @@ import Control.Monad
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (logWarn) import Control.Monad.Logger (logWarn)
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Data.Aeson (encode)
import Data.Bifunctor import Data.Bifunctor
import Data.Bool (bool) import Data.Bool (bool)
import Data.Default.Class (def) import Data.Default.Class (def)
@ -82,6 +83,7 @@ import Yesod.Form.Functions (runFormGet, runFormPost)
import Yesod.Form.Types (FormResult (..)) import Yesod.Form.Types (FormResult (..))
import Yesod.Persist.Core (runDB, get404, getBy404) import Yesod.Persist.Core (runDB, get404, getBy404)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T (filter, intercalate, pack) import qualified Data.Text as T (filter, intercalate, pack)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -95,11 +97,13 @@ import Yesod.ActivityPub
import Yesod.Auth.Unverified import Yesod.Auth.Unverified
import Yesod.FedURI import Yesod.FedURI
import Yesod.Hashids import Yesod.Hashids
import Yesod.MonadSite
import qualified Web.ActivityPub as AP import qualified Web.ActivityPub as AP
import Data.Either.Local import Data.Either.Local
import Data.Maybe.Local (partitionMaybePairs) import Data.Maybe.Local (partitionMaybePairs)
import Data.Paginate.Local
import Database.Persist.Local import Database.Persist.Local
import Yesod.Persist.Local import Yesod.Persist.Local
@ -114,6 +118,7 @@ import Vervis.Model
import Vervis.Model.Ident import Vervis.Model.Ident
import Vervis.Model.Ticket import Vervis.Model.Ticket
import Vervis.Model.Workflow import Vervis.Model.Workflow
import Vervis.Paginate
import Yesod.RenderSource import Yesod.RenderSource
import Vervis.Settings import Vervis.Settings
import Vervis.Style import Vervis.Style
@ -125,23 +130,84 @@ import Vervis.Widget.Discussion (discussionW)
import Vervis.Widget.Sharer import Vervis.Widget.Sharer
import Vervis.Widget.Ticket import Vervis.Widget.Ticket
getTicketsR :: ShrIdent -> PrjIdent -> Handler Html getTicketsR :: ShrIdent -> PrjIdent -> Handler TypedContent
getTicketsR shr prj = do getTicketsR shr prj = selectRep $ do
((filtResult, filtWidget), filtEnctype) <- runFormGet ticketFilterForm provideRep $ do
let tf = ((filtResult, filtWidget), filtEnctype) <- runFormGet ticketFilterForm
case filtResult of let tf =
FormSuccess filt -> filt case filtResult of
FormMissing -> def FormSuccess filt -> filt
FormFailure l -> FormMissing -> def
error $ "Ticket filter form failed: " ++ show l FormFailure l ->
rows <- runDB $ do error $ "Ticket filter form failed: " ++ show l
Entity sid _ <- getBy404 $ UniqueSharer shr (total, pages, mpage) <- runDB $ do
Entity jid _ <- getBy404 $ UniqueProject prj sid Entity sid _ <- getBy404 $ UniqueSharer shr
getTicketSummaries Entity jid _ <- getBy404 $ UniqueProject prj sid
(filterTickets tf) let countAllTickets = count [TicketProject ==. jid]
(Just $ \ t -> [E.asc $ t E.^. TicketNumber]) selectTickets off lim =
jid getTicketSummaries
defaultLayout $(widgetFile "ticket/list") (filterTickets tf)
(Just $ \ t -> [E.asc $ t E.^. TicketNumber])
(Just (off, lim))
jid
getPageAndNavCount countAllTickets selectTickets
case mpage of
Nothing -> redirectFirstPage here
Just (rows, navModel) ->
let pageNav = navWidget navModel
in defaultLayout $(widgetFile "ticket/list")
provideAP' $ do
(total, pages, mpage) <- runDB $ do
Entity sid _ <- getBy404 $ UniqueSharer shr
Entity jid _ <- getBy404 $ UniqueProject prj sid
let countAllTickets = count [TicketProject ==. jid]
selectTickets off lim = selectList [TicketProject ==. jid] [Desc TicketNumber, OffsetBy off, LimitTo lim]
getPageAndNavCount countAllTickets selectTickets
encodeRouteHome <- getEncodeRouteHome
encodeRouteLocal <- getEncodeRouteLocal
encodeRoutePageLocal <- getEncodeRoutePageLocal
let pageUrl = encodeRoutePageLocal here
host <- asksSite siteInstanceHost
return $
case mpage of
Nothing -> encodeStrict $ Doc host $ Collection
{ collectionId = encodeRouteLocal here
, collectionType = CollectionTypeOrdered
, collectionTotalItems = Just total
, collectionCurrent = Nothing
, collectionFirst = Just $ pageUrl 1
, collectionLast = Just $ pageUrl pages
, collectionItems = [] :: [Text]
}
Just (tickets, navModel) ->
let current = nmCurrent navModel
in encodeStrict $ Doc host $ CollectionPage
{ collectionPageId = pageUrl current
, collectionPageType = CollectionPageTypeOrdered
, collectionPageTotalItems = Nothing
, collectionPageCurrent = Just $ pageUrl current
, collectionPageFirst = Just $ pageUrl 1
, collectionPageLast = Just $ pageUrl pages
, collectionPagePartOf = encodeRouteLocal here
, collectionPagePrev =
if current > 1
then Just $ pageUrl $ current - 1
else Nothing
, collectionPageNext =
if current < pages
then Just $ pageUrl $ current + 1
else Nothing
, collectionPageStartIndex = Nothing
, collectionPageItems =
map (encodeRouteHome . ticketUrl . entityVal)
tickets
}
where
here = TicketsR shr prj
ticketUrl = TicketR shr prj . ticketNumber
encodeStrict = BL.toStrict . encode
getTicketTreeR :: ShrIdent -> PrjIdent -> Handler Html getTicketTreeR :: ShrIdent -> PrjIdent -> Handler Html
getTicketTreeR shr prj = do getTicketTreeR shr prj = do
@ -149,7 +215,7 @@ getTicketTreeR shr prj = do
Entity sid _ <- getBy404 $ UniqueSharer shr Entity sid _ <- getBy404 $ UniqueSharer shr
Entity jid _ <- getBy404 $ UniqueProject prj sid Entity jid _ <- getBy404 $ UniqueProject prj sid
liftA2 (,) liftA2 (,)
(getTicketSummaries Nothing Nothing jid) (getTicketSummaries Nothing Nothing Nothing jid)
(getTicketDepEdges jid) (getTicketDepEdges jid)
defaultLayout $ ticketTreeDW shr prj summaries deps defaultLayout $ ticketTreeDW shr prj summaries deps

View file

@ -161,6 +161,7 @@ postWorkflowFieldsR shr wfl = do
, workflowFieldFilterNew = nfNew nf , workflowFieldFilterNew = nfNew nf
, workflowFieldFilterTodo = nfTodo nf , workflowFieldFilterTodo = nfTodo nf
, workflowFieldFilterClosed = nfClosed nf , workflowFieldFilterClosed = nfClosed nf
, workflowFieldColor = nfColor nf
} }
runDB $ insert_ field runDB $ insert_ field
return $ Right $ nfIdent nf return $ Right $ nfIdent nf

View file

@ -1226,6 +1226,8 @@ changes hLocal ctx =
, renameUnique "WorkflowEnumCtor" "UniqueWorkflowFieldEnumCtor" "UniqueWorkflowEnumCtor" , renameUnique "WorkflowEnumCtor" "UniqueWorkflowFieldEnumCtor" "UniqueWorkflowEnumCtor"
-- 184 -- 184
, addEntities model_2020_01_05 , addEntities model_2020_01_05
-- 185
, addFieldPrimOptional "WorkflowField" (Nothing :: Maybe Int) "color"
] ]
migrateDB migrateDB

View file

@ -33,8 +33,8 @@ where
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Foldable (for_) import Data.Foldable (for_)
import Data.Text (Text) import Data.Text (Text)
import Data.Traversable
import Database.Esqueleto import Database.Esqueleto
import Database.Esqueleto.PostgreSQL
import Vervis.Foundation (AppDB) import Vervis.Foundation (AppDB)
import Vervis.Model import Vervis.Model
@ -46,63 +46,69 @@ import Vervis.Widget.Ticket (TicketSummary (..))
getTicketSummaries getTicketSummaries
:: Maybe (SqlExpr (Entity Ticket) -> SqlExpr (Value Bool)) :: Maybe (SqlExpr (Entity Ticket) -> SqlExpr (Value Bool))
-> Maybe (SqlExpr (Entity Ticket) -> [SqlExpr OrderBy]) -> Maybe (SqlExpr (Entity Ticket) -> [SqlExpr OrderBy])
-> Maybe (Int, Int)
-> ProjectId -> ProjectId
-> AppDB [TicketSummary] -> AppDB [TicketSummary]
getTicketSummaries mfilt morder jid = fmap (map toSummary) $ select $ from $ getTicketSummaries mfilt morder offlim jid = do
\ ( t tickets <- select $ from $
`LeftOuterJoin` (tal `InnerJoin` p `InnerJoin` s) \ ( t
`LeftOuterJoin` (tar `InnerJoin` ra `InnerJoin` ro `InnerJoin` i) `LeftOuterJoin` (tal `InnerJoin` p `InnerJoin` s)
`InnerJoin` d `LeftOuterJoin` (tar `InnerJoin` ra `InnerJoin` ro `InnerJoin` i)
`LeftOuterJoin` m `InnerJoin` d
`LeftOuterJoin` (tpc `InnerJoin` wf) `LeftOuterJoin` m
) -> do ) -> do
on $ tpc ?. TicketParamClassField ==. wf ?. WorkflowFieldId on $ just (d ^. DiscussionId) ==. m ?. MessageRoot
on $ just (t ^. TicketId) ==. tpc ?. TicketParamClassTicket on $ t ^. TicketDiscuss ==. d ^. DiscussionId
on $ just (d ^. DiscussionId) ==. m ?. MessageRoot on $ ro ?. RemoteObjectInstance ==. i ?. InstanceId
on $ t ^. TicketDiscuss ==. d ^. DiscussionId on $ ra ?. RemoteActorIdent ==. ro ?. RemoteObjectId
on $ ro ?. RemoteObjectInstance ==. i ?. InstanceId on $ tar ?. TicketAuthorRemoteAuthor ==. ra ?. RemoteActorId
on $ ra ?. RemoteActorIdent ==. ro ?. RemoteObjectId on $ just (t ^. TicketId) ==. tar ?. TicketAuthorRemoteTicket
on $ tar ?. TicketAuthorRemoteAuthor ==. ra ?. RemoteActorId on $ p ?. PersonIdent ==. s ?. SharerId
on $ just (t ^. TicketId) ==. tar ?. TicketAuthorRemoteTicket on $ tal ?. TicketAuthorLocalAuthor ==. p ?. PersonId
on $ p ?. PersonIdent ==. s ?. SharerId on $ just (t ^. TicketId) ==. tal ?. TicketAuthorLocalTicket
on $ tal ?. TicketAuthorLocalAuthor ==. p ?. PersonId where_ $ t ^. TicketProject ==. val jid
on $ just (t ^. TicketId) ==. tal ?. TicketAuthorLocalTicket groupBy
where_ $ t ^. TicketProject ==. val jid ( t ^. TicketId, s ?. SharerId
groupBy , ra ?. RemoteActorId, ro ?. RemoteObjectId, i ?. InstanceId
( t ^. TicketId, s ?. SharerId )
, ra ?. RemoteActorId, ro ?. RemoteObjectId, i ?. InstanceId for_ mfilt $ \ filt -> where_ $ filt t
) for_ morder $ \ order -> orderBy $ order t
for_ mfilt $ \ filt -> where_ $ filt t for_ offlim $ \ (off, lim) -> do
for_ morder $ \ order -> orderBy $ order t offset $ fromIntegral off
return limit $ fromIntegral lim
( t ^. TicketNumber return
, s ( t ^. TicketId
, i , t ^. TicketNumber
, ro , s
, ra , i
, t ^. TicketCreated , ro
, t ^. TicketTitle , ra
, t ^. TicketStatus , t ^. TicketCreated
, count $ m ?. MessageId , t ^. TicketTitle
, arrayRemoveNull $ maybeArray $ arrayAgg $ wf ?. WorkflowFieldName , t ^. TicketStatus
) , count $ m ?. MessageId
where )
toSummary (Value n, ms, mi, mro, mra, Value c, Value t, Value d, Value r, Value wfs) = for tickets $
TicketSummary \ (Value tid, Value n, ms, mi, mro, mra, Value c, Value t, Value d, Value r) -> do
{ tsNumber = n labels <- select $ from $ \ (tpc `InnerJoin` wf) -> do
, tsCreatedBy = on $ tpc ^. TicketParamClassField ==. wf ^. WorkflowFieldId
case (ms, mi, mro, mra) of where_ $ tpc ^. TicketParamClassTicket ==. val tid
(Just s, Nothing, Nothing, Nothing) -> return wf
Left $ entityVal s return TicketSummary
(Nothing, Just i, Just ro, Just ra) -> { tsNumber = n
Right (entityVal i, entityVal ro, entityVal ra) , tsCreatedBy =
_ -> error "Ticket author DB invalid state" case (ms, mi, mro, mra) of
, tsCreatedAt = c (Just s, Nothing, Nothing, Nothing) ->
, tsTitle = t Left $ entityVal s
, tsLabels = wfs (Nothing, Just i, Just ro, Just ra) ->
, tsStatus = d Right (entityVal i, entityVal ro, entityVal ra)
, tsComments = r _ -> error "Ticket author DB invalid state"
} , tsCreatedAt = c
, tsTitle = t
, tsLabels = map entityVal labels
, tsStatus = d
, tsComments = r
}
-- | Get the child-parent ticket number pairs of all the ticket dependencies -- | Get the child-parent ticket number pairs of all the ticket dependencies
-- in the given project, in ascending order by child, and then ascending order -- in the given project, in ascending order by child, and then ascending order

View file

@ -50,7 +50,7 @@ data TicketSummary = TicketSummary
, tsCreatedBy :: Either Sharer (Instance, RemoteObject, RemoteActor) , tsCreatedBy :: Either Sharer (Instance, RemoteObject, RemoteActor)
, tsCreatedAt :: UTCTime , tsCreatedAt :: UTCTime
, tsTitle :: Text , tsTitle :: Text
, tsLabels :: [Text] , tsLabels :: [WorkflowField]
, tsStatus :: TicketStatus , tsStatus :: TicketStatus
, tsComments :: Int , tsComments :: Int
} }

View file

@ -23,6 +23,10 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<div class="submit"> <div class="submit">
<input type="submit" value="Filter"> <input type="submit" value="Filter">
^{pageNav}
<div .container> <div .container>
$forall ts <- rows $forall ts <- rows
^{ticketSummaryW shr prj ts Nothing} ^{ticketSummaryW shr prj ts Nothing}
^{pageNav}

View file

@ -1,6 +1,7 @@
/* This file is part of Vervis. /* This file is part of Vervis.
* *
* Written in 2016 by fr33domlover <fr33domlover@riseup.net>. * Written in 2016, 2020 by fr33domlover <fr33domlover@riseup.net>,
* 2019 by Jason Harrer <jazzyeagle79@gmail.com>.
* *
* ♡ Copying is an act of love. Please copy, reuse and share. * ♡ Copying is an act of love. Please copy, reuse and share.
* *
@ -24,7 +25,7 @@
.ticket-status-column .ticket-status-column
grid-column: 1 / 1 grid-column: 1 / 1
"
.ticket-number-column .ticket-number-column
grid-column: 2 / 2 grid-column: 2 / 2
@ -42,3 +43,15 @@
.ticket-node-column .ticket-node-column
grid-column: 7 / 7 grid-column: 7 / 7
.label1
color: #{light red}
.label2
color: #{light green}
.label3
color: #{light yellow}
.label4
color: #{light blue}

View file

@ -37,8 +37,13 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<span .ticket-title-column> <span .ticket-title-column>
<a href=@{TicketR shr prj $ tsNumber ts}> <a href=@{TicketR shr prj $ tsNumber ts}>
#{tsTitle ts} #{tsTitle ts}
$forall wfname <- tsLabels ts $forall wf <- tsLabels ts
\ [#{wfname}] $maybe wfcol <- workflowFieldColor wf
<span .label#{wfcol}>
[#{workflowFieldName wf}]
$nothing
<span .label-nocolor>
[#{workflowFieldName wf}]
<span .ticket-tree-column> <span .ticket-tree-column>
$if tsComments ts > 0 $if tsComments ts > 0

View file

@ -1,6 +1,6 @@
$# This file is part of Vervis. $# This file is part of Vervis.
$# $#
$# Written in 2016, 2018 by fr33domlover <fr33domlover@riseup.net>. $# Written in 2016, 2018, 2020 by fr33domlover <fr33domlover@riseup.net>.
$# $#
$# ♡ Copying is an act of love. Please copy, reuse and share. $# ♡ Copying is an act of love. Please copy, reuse and share.
$# $#
@ -38,3 +38,9 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
Applies to Todo tickets: #{workflowFieldFilterTodo f} Applies to Todo tickets: #{workflowFieldFilterTodo f}
<li> <li>
Applies to Closed tickets: #{workflowFieldFilterClosed f} Applies to Closed tickets: #{workflowFieldFilterClosed f}
<li>
Color:
$maybe c <- workflowFieldColor f
#{c}
$nothing
None